Browse Source

Move to fasthttp for improved performance

oauth2
Morgan Bazalgette 2 years ago
parent
commit
85e6dc7e5e

+ 4
- 10
app/internals/status.go View File

@@ -1,17 +1,11 @@
1 1
 // Package internals has methods that suit none of the API packages.
2 2
 package internals
3 3
 
4
-import (
5
-	"github.com/gin-gonic/gin"
6
-)
4
+import "github.com/valyala/fasthttp"
7 5
 
8
-type statusResponse struct {
9
-	Status int `json:"status"`
10
-}
6
+var statusResp = []byte(`{ "status": 1 }`)
11 7
 
12 8
 // Status is used for checking the API is up by the ripple website, on the status page.
13
-func Status(c *gin.Context) {
14
-	c.JSON(200, statusResponse{
15
-		Status: 1,
16
-	})
9
+func Status(c *fasthttp.RequestCtx) {
10
+	c.Write(statusResp)
17 11
 }

+ 42
- 60
app/method.go View File

@@ -1,54 +1,43 @@
1 1
 package app
2 2
 
3 3
 import (
4
-	"crypto/md5"
5 4
 	"encoding/json"
6 5
 	"fmt"
7
-	"io/ioutil"
8
-	"net"
9 6
 	"regexp"
10
-	"strings"
7
+	"unsafe"
11 8
 
9
+	"github.com/valyala/fasthttp"
12 10
 	"zxq.co/ripple/rippleapi/common"
13
-	"github.com/gin-gonic/gin"
14 11
 )
15 12
 
16 13
 // Method wraps an API method to a HandlerFunc.
17
-func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) gin.HandlerFunc {
18
-	return func(c *gin.Context) {
14
+func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) fasthttp.RequestHandler {
15
+	return func(c *fasthttp.RequestCtx) {
19 16
 		initialCaretaker(c, f, privilegesNeeded...)
20 17
 	}
21 18
 }
22 19
 
23
-func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
24
-	rateLimiter()
25
-
20
+func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
26 21
 	var doggoTags []string
27 22
 
28
-	data, err := ioutil.ReadAll(c.Request.Body)
29
-	if err != nil {
30
-		c.Error(err)
31
-	}
32
-
33
-	token := ""
23
+	qa := c.Request.URI().QueryArgs()
24
+	var token string
34 25
 	switch {
35
-	case c.Request.Header.Get("X-Ripple-Token") != "":
36
-		token = c.Request.Header.Get("X-Ripple-Token")
37
-	case c.Query("token") != "":
38
-		token = c.Query("token")
39
-	case c.Query("k") != "":
40
-		token = c.Query("k")
26
+	case len(c.Request.Header.Peek("X-Ripple-Token")) > 0:
27
+		token = string(c.Request.Header.Peek("X-Ripple-Token"))
28
+	case len(qa.Peek("token")) > 0:
29
+		token = string(qa.Peek("token"))
30
+	case len(qa.Peek("k")) > 0:
31
+		token = string(qa.Peek("k"))
41 32
 	default:
42
-		token, _ = c.Cookie("rt")
33
+		token = string(c.Request.Header.Cookie("rt"))
43 34
 	}
44
-	c.Set("token", fmt.Sprintf("%x", md5.Sum([]byte(token))))
45 35
 
46 36
 	md := common.MethodData{
47
-		DB:          db,
48
-		RequestData: data,
49
-		C:           c,
50
-		Doggo:       doggo,
51
-		R:           red,
37
+		DB:    db,
38
+		Ctx:   c,
39
+		Doggo: doggo,
40
+		R:     red,
52 41
 	}
53 42
 	if token != "" {
54 43
 		tokenReal, exists := GetTokenFull(token, db)
@@ -58,25 +47,8 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe
58 47
 		}
59 48
 	}
60 49
 
61
-	var ip string
62
-	if requestIP, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err != nil {
63
-		panic(err)
64
-	} else {
65
-		// if requestIP is not 127.0.0.1, means no reverse proxy is being used => direct request.
66
-		if requestIP != "127.0.0.1" {
67
-			ip = requestIP
68
-		}
69
-	}
70
-
71
-	// means we're using reverse-proxy, so X-Real-IP
72
-	if ip == "" {
73
-		ip = c.ClientIP()
74
-	}
75
-
76
-	// requests from hanayo should not be rate limited.
77
-	if !(c.Request.Header.Get("H-Key") == cf.HanayoKey && c.Request.UserAgent() == "hanayo") {
78
-		perUserRequestLimiter(md.ID(), c.ClientIP())
79
-	} else {
50
+	// log into datadog that this is an hanayo request
51
+	if b2s(c.Request.Header.Peek("H-Key")) == cf.HanayoKey && b2s(c.UserAgent()) == "hanayo" {
80 52
 		doggoTags = append(doggoTags, "hanayo")
81 53
 	}
82 54
 
@@ -89,21 +61,22 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe
89 61
 		}
90 62
 	}
91 63
 	if missingPrivileges != 0 {
92
-		c.IndentedJSON(401, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
64
+		c.SetStatusCode(401)
65
+		mkjson(c, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
93 66
 		return
94 67
 	}
95 68
 
96 69
 	resp := f(md)
97 70
 	if md.HasQuery("pls200") {
98
-		c.Writer.WriteHeader(200)
71
+		c.SetStatusCode(200)
99 72
 	} else {
100
-		c.Writer.WriteHeader(resp.GetCode())
73
+		c.SetStatusCode(resp.GetCode())
101 74
 	}
102 75
 
103 76
 	if md.HasQuery("callback") {
104
-		c.Header("Content-Type", "application/javascript; charset=utf-8")
77
+		c.Response.Header.Add("Content-Type", "application/javascript; charset=utf-8")
105 78
 	} else {
106
-		c.Header("Content-Type", "application/json; charset=utf-8")
79
+		c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
107 80
 	}
108 81
 
109 82
 	mkjson(c, resp)
@@ -113,22 +86,31 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe
113 86
 var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
114 87
 
115 88
 // mkjson auto indents json, and wraps json into a jsonp callback if specified by the request.
116
-// then writes to the gin.Context the data.
117
-func mkjson(c *gin.Context, data interface{}) {
89
+// then writes to the RequestCtx the data.
90
+func mkjson(c *fasthttp.RequestCtx, data interface{}) {
118 91
 	exported, err := json.MarshalIndent(data, "", "\t")
119 92
 	if err != nil {
120
-		c.Error(err)
93
+		fmt.Println(err)
121 94
 		exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong." }`)
122 95
 	}
123
-	cb := c.Query("callback")
96
+	cb := string(c.URI().QueryArgs().Peek("callback"))
124 97
 	willcb := cb != "" &&
125 98
 		len(cb) < 100 &&
126 99
 		callbackJSONP.MatchString(cb)
127 100
 	if willcb {
128
-		c.Writer.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
101
+		c.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
129 102
 	}
130
-	c.Writer.Write(exported)
103
+	c.Write(exported)
131 104
 	if willcb {
132
-		c.Writer.Write([]byte(");"))
105
+		c.Write([]byte(");"))
133 106
 	}
134 107
 }
108
+
109
+// b2s converts byte slice to a string without memory allocation.
110
+// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
111
+//
112
+// Note it may break if string and/or slice header will change
113
+// in the future go versions.
114
+func b2s(b []byte) string {
115
+	return *(*string)(unsafe.Pointer(&b))
116
+}

+ 17
- 17
app/peppy/beatmap.go View File

@@ -4,32 +4,32 @@ import (
4 4
 	"strconv"
5 5
 	"strings"
6 6
 
7
-	"zxq.co/ripple/rippleapi/common"
8
-	"github.com/gin-gonic/gin"
9 7
 	"github.com/jmoiron/sqlx"
10 8
 	"github.com/thehowl/go-osuapi"
9
+	"github.com/valyala/fasthttp"
10
+	"zxq.co/ripple/rippleapi/common"
11 11
 )
12 12
 
13 13
 // GetBeatmap retrieves general beatmap information.
14
-func GetBeatmap(c *gin.Context, db *sqlx.DB) {
14
+func GetBeatmap(c *fasthttp.RequestCtx, db *sqlx.DB) {
15 15
 	var whereClauses []string
16 16
 	var params []interface{}
17
-	limit := strconv.Itoa(common.InString(1, c.Query("limit"), 500, 500))
17
+	limit := strconv.Itoa(common.InString(1, query(c, "limit"), 500, 500))
18 18
 
19 19
 	// since value is not stored, silently ignore
20
-	if c.Query("s") != "" {
20
+	if query(c, "s") != "" {
21 21
 		whereClauses = append(whereClauses, "beatmaps.beatmapset_id = ?")
22
-		params = append(params, c.Query("s"))
22
+		params = append(params, query(c, "s"))
23 23
 	}
24
-	if c.Query("b") != "" {
24
+	if query(c, "b") != "" {
25 25
 		whereClauses = append(whereClauses, "beatmaps.beatmap_id = ?")
26
-		params = append(params, c.Query("b"))
26
+		params = append(params, query(c, "b"))
27 27
 		// b is unique, so change limit to 1
28 28
 		limit = "1"
29 29
 	}
30 30
 	// creator is not stored, silently ignore u and type
31
-	if c.Query("m") != "" {
32
-		m := genmode(c.Query("m"))
31
+	if query(c, "m") != "" {
32
+		m := genmode(query(c, "m"))
33 33
 		if m == "std" {
34 34
 			// Since STD beatmaps are converted, all of the diffs must be != 0
35 35
 			for _, i := range modes {
@@ -37,14 +37,14 @@ func GetBeatmap(c *gin.Context, db *sqlx.DB) {
37 37
 			}
38 38
 		} else {
39 39
 			whereClauses = append(whereClauses, "beatmaps.difficulty_"+m+" != 0")
40
-			if c.Query("a") == "1" {
40
+			if query(c, "a") == "1" {
41 41
 				whereClauses = append(whereClauses, "beatmaps.difficulty_std = 0")
42 42
 			}
43 43
 		}
44 44
 	}
45
-	if c.Query("h") != "" {
45
+	if query(c, "h") != "" {
46 46
 		whereClauses = append(whereClauses, "beatmaps.beatmap_md5 = ?")
47
-		params = append(params, c.Query("h"))
47
+		params = append(params, query(c, "h"))
48 48
 	}
49 49
 
50 50
 	where := strings.Join(whereClauses, " AND ")
@@ -61,8 +61,8 @@ func GetBeatmap(c *gin.Context, db *sqlx.DB) {
61 61
 FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
62 62
 		params...)
63 63
 	if err != nil {
64
-		c.Error(err)
65
-		c.JSON(200, defaultResponse)
64
+		common.Err(c, err)
65
+		json(c, 200, defaultResponse)
66 66
 		return
67 67
 	}
68 68
 
@@ -82,7 +82,7 @@ FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
82 82
 			&rawLastUpdate,
83 83
 		)
84 84
 		if err != nil {
85
-			c.Error(err)
85
+			common.Err(c, err)
86 86
 			continue
87 87
 		}
88 88
 		bm.TotalLength = bm.HitLength
@@ -103,7 +103,7 @@ FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
103 103
 		bms = append(bms, bm)
104 104
 	}
105 105
 
106
-	c.JSON(200, bms)
106
+	json(c, 200, bms)
107 107
 }
108 108
 
109 109
 var rippleToOsuRankedStatus = map[int]osuapi.ApprovedStatus{

+ 22
- 9
app/peppy/common.go View File

@@ -2,12 +2,12 @@ package peppy
2 2
 
3 3
 import (
4 4
 	"database/sql"
5
+	_json "encoding/json"
5 6
 	"strconv"
6 7
 
7
-	"zxq.co/ripple/rippleapi/common"
8
-
9
-	"github.com/gin-gonic/gin"
10 8
 	"github.com/jmoiron/sqlx"
9
+	"github.com/valyala/fasthttp"
10
+	"zxq.co/ripple/rippleapi/common"
11 11
 )
12 12
 
13 13
 var modes = []string{"std", "taiko", "ctb", "mania"}
@@ -30,25 +30,25 @@ func rankable(m string) bool {
30 30
 	return x == 0 || x == 3
31 31
 }
32 32
 
33
-func genUser(c *gin.Context, db *sqlx.DB) (string, string) {
33
+func genUser(c *fasthttp.RequestCtx, db *sqlx.DB) (string, string) {
34 34
 	var whereClause string
35 35
 	var p string
36 36
 
37 37
 	// used in second case of switch
38
-	s, err := strconv.Atoi(c.Query("u"))
38
+	s, err := strconv.Atoi(query(c, "u"))
39 39
 
40 40
 	switch {
41 41
 	// We know for sure that it's an username.
42
-	case c.Query("type") == "string":
42
+	case query(c, "type") == "string":
43 43
 		whereClause = "users.username_safe = ?"
44
-		p = common.SafeUsername(c.Query("u"))
44
+		p = common.SafeUsername(query(c, "u"))
45 45
 	// It could be an user ID, so we look for an user with that username first.
46 46
 	case err == nil:
47 47
 		err = db.QueryRow("SELECT id FROM users WHERE id = ? LIMIT 1", s).Scan(&p)
48 48
 		if err == sql.ErrNoRows {
49 49
 			// If no user with that userID were found, assume username.
50 50
 			whereClause = "users.username_safe = ?"
51
-			p = common.SafeUsername(c.Query("u"))
51
+			p = common.SafeUsername(query(c, "u"))
52 52
 		} else {
53 53
 			// An user with that userID was found. Thus it's an userID.
54 54
 			whereClause = "users.id = ?"
@@ -56,7 +56,20 @@ func genUser(c *gin.Context, db *sqlx.DB) (string, string) {
56 56
 	// u contains letters, so it's an username.
57 57
 	default:
58 58
 		whereClause = "users.username_safe = ?"
59
-		p = common.SafeUsername(c.Query("u"))
59
+		p = common.SafeUsername(query(c, "u"))
60 60
 	}
61 61
 	return whereClause, p
62 62
 }
63
+
64
+func query(c *fasthttp.RequestCtx, s string) string {
65
+	return string(c.QueryArgs().Peek(s))
66
+}
67
+
68
+func json(c *fasthttp.RequestCtx, code int, data interface{}) {
69
+	c.SetStatusCode(code)
70
+	d, err := _json.Marshal(data)
71
+	if err != nil {
72
+		panic(err)
73
+	}
74
+	c.Write(d)
75
+}

+ 3
- 3
app/peppy/match.go View File

@@ -2,11 +2,11 @@
2 2
 package peppy
3 3
 
4 4
 import (
5
-	"github.com/gin-gonic/gin"
6 5
 	"github.com/jmoiron/sqlx"
6
+	"github.com/valyala/fasthttp"
7 7
 )
8 8
 
9 9
 // GetMatch retrieves general match information.
10
-func GetMatch(c *gin.Context, db *sqlx.DB) {
11
-	c.JSON(200, defaultResponse)
10
+func GetMatch(c *fasthttp.RequestCtx, db *sqlx.DB) {
11
+	json(c, 200, defaultResponse)
12 12
 }

+ 19
- 19
app/peppy/score.go View File

@@ -7,43 +7,43 @@ import (
7 7
 
8 8
 	"zxq.co/ripple/rippleapi/common"
9 9
 
10
-	"zxq.co/x/getrank"
11
-	"github.com/gin-gonic/gin"
12 10
 	"github.com/jmoiron/sqlx"
11
+	"github.com/valyala/fasthttp"
13 12
 	"gopkg.in/thehowl/go-osuapi.v1"
13
+	"zxq.co/x/getrank"
14 14
 )
15 15
 
16 16
 // GetScores retrieve information about the top 100 scores of a specified beatmap.
17
-func GetScores(c *gin.Context, db *sqlx.DB) {
18
-	if c.Query("b") == "" {
19
-		c.JSON(200, defaultResponse)
17
+func GetScores(c *fasthttp.RequestCtx, db *sqlx.DB) {
18
+	if query(c, "b") == "" {
19
+		json(c, 200, defaultResponse)
20 20
 		return
21 21
 	}
22 22
 	var beatmapMD5 string
23
-	err := db.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", c.Query("b"))
23
+	err := db.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", query(c, "b"))
24 24
 	switch {
25 25
 	case err == sql.ErrNoRows:
26
-		c.JSON(200, defaultResponse)
26
+		json(c, 200, defaultResponse)
27 27
 		return
28 28
 	case err != nil:
29
-		c.Error(err)
30
-		c.JSON(200, defaultResponse)
29
+		common.Err(c, err)
30
+		json(c, 200, defaultResponse)
31 31
 		return
32 32
 	}
33 33
 	var sb = "scores.score"
34
-	if rankable(c.Query("m")) {
34
+	if rankable(query(c, "m")) {
35 35
 		sb = "scores.pp"
36 36
 	}
37 37
 	var (
38 38
 		extraWhere  string
39 39
 		extraParams []interface{}
40 40
 	)
41
-	if c.Query("u") != "" {
41
+	if query(c, "u") != "" {
42 42
 		w, p := genUser(c, db)
43 43
 		extraWhere = "AND " + w
44 44
 		extraParams = append(extraParams, p)
45 45
 	}
46
-	mods := common.Int(c.Query("mods"))
46
+	mods := common.Int(query(c, "mods"))
47 47
 	rows, err := db.Query(`
48 48
 SELECT
49 49
 	scores.id, scores.score, users.username, scores.300_count, scores.100_count,
@@ -58,11 +58,11 @@ WHERE scores.completed = '3'
58 58
   AND scores.play_mode = ?
59 59
   AND scores.mods & ? = ?
60 60
   `+extraWhere+`
61
-ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, c.Query("limit"), 100, 50)),
62
-		append([]interface{}{beatmapMD5, genmodei(c.Query("m")), mods, mods}, extraParams...)...)
61
+ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, query(c, "limit"), 100, 50)),
62
+		append([]interface{}{beatmapMD5, genmodei(query(c, "m")), mods, mods}, extraParams...)...)
63 63
 	if err != nil {
64
-		c.Error(err)
65
-		c.JSON(200, defaultResponse)
64
+		common.Err(c, err)
65
+		json(c, 200, defaultResponse)
66 66
 		return
67 67
 	}
68 68
 	var results []osuapi.GSScore
@@ -82,17 +82,17 @@ ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, c.Query("limit"), 1
82 82
 		)
83 83
 		if err != nil {
84 84
 			if err != sql.ErrNoRows {
85
-				c.Error(err)
85
+				common.Err(c, err)
86 86
 			}
87 87
 			continue
88 88
 		}
89 89
 		s.FullCombo = osuapi.OsuBool(fullcombo)
90 90
 		s.Mods = osuapi.Mods(mods)
91 91
 		s.Date = osuapi.MySQLDate(date)
92
-		s.Rank = strings.ToUpper(getrank.GetRank(osuapi.Mode(genmodei(c.Query("m"))), s.Mods,
92
+		s.Rank = strings.ToUpper(getrank.GetRank(osuapi.Mode(genmodei(query(c, "m"))), s.Mods,
93 93
 			accuracy, s.Count300, s.Count100, s.Count50, s.CountMiss))
94 94
 		results = append(results, s)
95 95
 	}
96
-	c.JSON(200, results)
96
+	json(c, 200, results)
97 97
 	return
98 98
 }

+ 10
- 9
app/peppy/user.go View File

@@ -5,23 +5,24 @@ import (
5 5
 	"database/sql"
6 6
 	"fmt"
7 7
 
8
-	"zxq.co/ripple/ocl"
9
-	"github.com/gin-gonic/gin"
10 8
 	"github.com/jmoiron/sqlx"
11 9
 	"github.com/thehowl/go-osuapi"
10
+	"github.com/valyala/fasthttp"
11
+	"zxq.co/ripple/ocl"
12
+	"zxq.co/ripple/rippleapi/common"
12 13
 )
13 14
 
14 15
 // GetUser retrieves general user information.
15
-func GetUser(c *gin.Context, db *sqlx.DB) {
16
-	if c.Query("u") == "" {
17
-		c.JSON(200, defaultResponse)
16
+func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
17
+	if query(c, "u") == "" {
18
+		json(c, 200, defaultResponse)
18 19
 		return
19 20
 	}
20 21
 	var user osuapi.User
21 22
 	whereClause, p := genUser(c, db)
22 23
 	whereClause = "WHERE " + whereClause
23 24
 
24
-	mode := genmode(c.Query("m"))
25
+	mode := genmode(query(c, "m"))
25 26
 
26 27
 	var lbpos *int
27 28
 	err := db.QueryRow(fmt.Sprintf(
@@ -43,9 +44,9 @@ func GetUser(c *gin.Context, db *sqlx.DB) {
43 44
 		&user.Country,
44 45
 	)
45 46
 	if err != nil {
46
-		c.JSON(200, defaultResponse)
47
+		json(c, 200, defaultResponse)
47 48
 		if err != sql.ErrNoRows {
48
-			c.Error(err)
49
+			common.Err(c, err)
49 50
 		}
50 51
 		return
51 52
 	}
@@ -54,5 +55,5 @@ func GetUser(c *gin.Context, db *sqlx.DB) {
54 55
 	}
55 56
 	user.Level = ocl.GetLevelPrecise(user.TotalScore)
56 57
 
57
-	c.JSON(200, []osuapi.User{user})
58
+	json(c, 200, []osuapi.User{user})
58 59
 }

+ 17
- 17
app/peppy/user_x.go View File

@@ -4,32 +4,32 @@ import (
4 4
 	"fmt"
5 5
 	"strings"
6 6
 
7
-	"zxq.co/ripple/rippleapi/common"
8
-	"zxq.co/x/getrank"
9
-	"github.com/gin-gonic/gin"
10 7
 	"github.com/jmoiron/sqlx"
8
+	"github.com/valyala/fasthttp"
11 9
 	"gopkg.in/thehowl/go-osuapi.v1"
10
+	"zxq.co/ripple/rippleapi/common"
11
+	"zxq.co/x/getrank"
12 12
 )
13 13
 
14 14
 // GetUserRecent retrieves an user's recent scores.
15
-func GetUserRecent(c *gin.Context, db *sqlx.DB) {
16
-	getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, c.Query("limit"), 50, 10))
15
+func GetUserRecent(c *fasthttp.RequestCtx, db *sqlx.DB) {
16
+	getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, query(c, "limit"), 50, 10))
17 17
 }
18 18
 
19 19
 // GetUserBest retrieves an user's best scores.
20
-func GetUserBest(c *gin.Context, db *sqlx.DB) {
20
+func GetUserBest(c *fasthttp.RequestCtx, db *sqlx.DB) {
21 21
 	var sb string
22
-	if rankable(c.Query("m")) {
22
+	if rankable(query(c, "m")) {
23 23
 		sb = "scores.pp"
24 24
 	} else {
25 25
 		sb = "scores.score"
26 26
 	}
27
-	getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, c.Query("limit"), 100, 10))
27
+	getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, query(c, "limit"), 100, 10))
28 28
 }
29 29
 
30
-func getUserX(c *gin.Context, db *sqlx.DB, orderBy string, limit int) {
30
+func getUserX(c *fasthttp.RequestCtx, db *sqlx.DB, orderBy string, limit int) {
31 31
 	whereClause, p := genUser(c, db)
32
-	query := fmt.Sprintf(
32
+	sqlQuery := fmt.Sprintf(
33 33
 		`SELECT
34 34
 			beatmaps.beatmap_id, scores.score, scores.max_combo,
35 35
 			scores.300_count, scores.100_count, scores.50_count,
@@ -44,11 +44,11 @@ func getUserX(c *gin.Context, db *sqlx.DB, orderBy string, limit int) {
44 44
 		LIMIT %d`, whereClause, orderBy, limit,
45 45
 	)
46 46
 	scores := make([]osuapi.GUSScore, 0, limit)
47
-	m := genmodei(c.Query("m"))
48
-	rows, err := db.Query(query, p, m)
47
+	m := genmodei(query(c, "m"))
48
+	rows, err := db.Query(sqlQuery, p, m)
49 49
 	if err != nil {
50
-		c.JSON(200, defaultResponse)
51
-		c.Error(err)
50
+		json(c, 200, defaultResponse)
51
+		common.Err(c, err)
52 52
 		return
53 53
 	}
54 54
 	for rows.Next() {
@@ -68,8 +68,8 @@ func getUserX(c *gin.Context, db *sqlx.DB, orderBy string, limit int) {
68 68
 			&curscore.PP, &acc,
69 69
 		)
70 70
 		if err != nil {
71
-			c.JSON(200, defaultResponse)
72
-			c.Error(err)
71
+			json(c, 200, defaultResponse)
72
+			common.Err(c, err)
73 73
 			return
74 74
 		}
75 75
 		if bid == nil {
@@ -91,5 +91,5 @@ func getUserX(c *gin.Context, db *sqlx.DB, orderBy string, limit int) {
91 91
 		))
92 92
 		scores = append(scores, curscore)
93 93
 	}
94
-	c.JSON(200, scores)
94
+	json(c, 200, scores)
95 95
 }

+ 3
- 6
app/peppy_method.go View File

@@ -1,16 +1,13 @@
1 1
 package app
2 2
 
3 3
 import (
4
-	"github.com/gin-gonic/gin"
5 4
 	"github.com/jmoiron/sqlx"
5
+	"github.com/valyala/fasthttp"
6 6
 )
7 7
 
8 8
 // PeppyMethod generates a method for the peppyapi
9
-func PeppyMethod(a func(c *gin.Context, db *sqlx.DB)) gin.HandlerFunc {
10
-	return func(c *gin.Context) {
11
-		rateLimiter()
12
-		perUserRequestLimiter(0, c.ClientIP())
13
-
9
+func PeppyMethod(a func(c *fasthttp.RequestCtx, db *sqlx.DB)) fasthttp.RequestHandler {
10
+	return func(c *fasthttp.RequestCtx) {
14 11
 		doggo.Incr("requests.peppy", nil, 1)
15 12
 
16 13
 		// I have no idea how, but I manged to accidentally string the first 4

+ 0
- 36
app/rate_limiter.go View File

@@ -1,36 +0,0 @@
1
-package app
2
-
3
-import (
4
-	"strconv"
5
-	"time"
6
-
7
-	"zxq.co/ripple/rippleapi/limit"
8
-)
9
-
10
-const reqsPerSecond = 5000
11
-const sleepTime = time.Second / reqsPerSecond
12
-
13
-var limiter = make(chan struct{}, reqsPerSecond)
14
-
15
-func setUpLimiter() {
16
-	for i := 0; i < reqsPerSecond; i++ {
17
-		limiter <- struct{}{}
18
-	}
19
-	go func() {
20
-		for {
21
-			limiter <- struct{}{}
22
-			time.Sleep(sleepTime)
23
-		}
24
-	}()
25
-}
26
-
27
-func rateLimiter() {
28
-	<-limiter
29
-}
30
-func perUserRequestLimiter(uid int, ip string) {
31
-	if uid == 0 {
32
-		limit.Request("ip:"+ip, 200)
33
-	} else {
34
-		limit.Request("user:"+strconv.Itoa(uid), 3000)
35
-	}
36
-}

+ 93
- 94
app/start.go View File

@@ -4,9 +4,8 @@ import (
4 4
 	"fmt"
5 5
 
6 6
 	"github.com/DataDog/datadog-go/statsd"
7
+	fhr "github.com/buaazp/fasthttprouter"
7 8
 	"github.com/getsentry/raven-go"
8
-	"github.com/gin-gonic/contrib/gzip"
9
-	"github.com/gin-gonic/gin"
10 9
 	"github.com/jmoiron/sqlx"
11 10
 	"github.com/serenize/snaker"
12 11
 	"gopkg.in/redis.v5"
@@ -29,7 +28,7 @@ var commonClusterfucks = map[string]string{
29 28
 }
30 29
 
31 30
 // Start begins taking HTTP connections.
32
-func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
31
+func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
33 32
 	db = dbO
34 33
 	cf = conf
35 34
 
@@ -40,10 +39,10 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
40 39
 		return snaker.CamelToSnake(s)
41 40
 	})
42 41
 
43
-	setUpLimiter()
44
-
45
-	r := gin.Default()
46
-	r.Use(gzip.Gzip(gzip.DefaultCompression))
42
+	r := fhr.New()
43
+	// TODO: add back gzip
44
+	// TODO: add logging
45
+	// TODO: add sentry panic recovering
47 46
 
48 47
 	// sentry
49 48
 	if conf.SentryDSN != "" {
@@ -52,7 +51,8 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
52 51
 		if err != nil {
53 52
 			fmt.Println(err)
54 53
 		} else {
55
-			r.Use(Recovery(ravenClient, false))
54
+			// r.Use(Recovery(ravenClient, false))
55
+			common.RavenClient = ravenClient
56 56
 		}
57 57
 	}
58 58
 
@@ -63,9 +63,9 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
63 63
 		fmt.Println(err)
64 64
 	}
65 65
 	doggo.Namespace = "api."
66
-	r.Use(func(c *gin.Context) {
67
-		doggo.Incr("requests", nil, 1)
68
-	})
66
+	// r.Use(func(c *gin.Context) {
67
+	// 	doggo.Incr("requests", nil, 1)
68
+	// })
69 69
 
70 70
 	// redis
71 71
 	red = redis.NewClient(&redis.Options{
@@ -77,94 +77,93 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
77 77
 	// token updater
78 78
 	go tokenUpdater(db)
79 79
 
80
-	api := r.Group("/api")
80
+	// peppyapi
81 81
 	{
82
-		p := api.Group("/")
83
-		{
84
-			p.GET("/get_user", PeppyMethod(peppy.GetUser))
85
-			p.GET("/get_match", PeppyMethod(peppy.GetMatch))
86
-			p.GET("/get_user_recent", PeppyMethod(peppy.GetUserRecent))
87
-			p.GET("/get_user_best", PeppyMethod(peppy.GetUserBest))
88
-			p.GET("/get_scores", PeppyMethod(peppy.GetScores))
89
-			p.GET("/get_beatmaps", PeppyMethod(peppy.GetBeatmap))
90
-		}
82
+		r.GET("/api/get_user", PeppyMethod(peppy.GetUser))
83
+		r.GET("/api/get_match", PeppyMethod(peppy.GetMatch))
84
+		r.GET("/api/get_user_recent", PeppyMethod(peppy.GetUserRecent))
85
+		r.GET("/api/get_user_best", PeppyMethod(peppy.GetUserBest))
86
+		r.GET("/api/get_scores", PeppyMethod(peppy.GetScores))
87
+		r.GET("/api/get_beatmaps", PeppyMethod(peppy.GetBeatmap))
88
+	}
91 89
 
92
-		gv1 := api.Group("/v1")
93
-		{
94
-			gv1.POST("/tokens", Method(v1.TokenNewPOST))
95
-			gv1.POST("/tokens/new", Method(v1.TokenNewPOST))
96
-			gv1.POST("/tokens/self/delete", Method(v1.TokenSelfDeletePOST))
97
-
98
-			// Auth-free API endpoints (public data)
99
-			gv1.GET("/ping", Method(v1.PingGET))
100
-			gv1.GET("/surprise_me", Method(v1.SurpriseMeGET))
101
-			gv1.GET("/doc", Method(v1.DocGET))
102
-			gv1.GET("/doc/content", Method(v1.DocContentGET))
103
-			gv1.GET("/doc/rules", Method(v1.DocRulesGET))
104
-			gv1.GET("/users", Method(v1.UsersGET))
105
-			gv1.GET("/users/whatid", Method(v1.UserWhatsTheIDGET))
106
-			gv1.GET("/users/full", Method(v1.UserFullGET))
107
-			gv1.GET("/users/userpage", Method(v1.UserUserpageGET))
108
-			gv1.GET("/users/lookup", Method(v1.UserLookupGET))
109
-			gv1.GET("/users/scores/best", Method(v1.UserScoresBestGET))
110
-			gv1.GET("/users/scores/recent", Method(v1.UserScoresRecentGET))
111
-			gv1.GET("/badges", Method(v1.BadgesGET))
112
-			gv1.GET("/beatmaps", Method(v1.BeatmapGET))
113
-			gv1.GET("/leaderboard", Method(v1.LeaderboardGET))
114
-			gv1.GET("/tokens", Method(v1.TokenGET))
115
-			gv1.GET("/users/self", Method(v1.UserSelfGET))
116
-			gv1.GET("/tokens/self", Method(v1.TokenSelfGET))
117
-			gv1.GET("/blog/posts", Method(v1.BlogPostsGET))
118
-			gv1.GET("/scores", Method(v1.ScoresGET))
119
-			gv1.GET("/beatmaps/rank_requests/status", Method(v1.BeatmapRankRequestsStatusGET))
120
-
121
-			// ReadConfidential privilege required
122
-			gv1.GET("/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential))
123
-			gv1.GET("/friends/with", Method(v1.FriendsWithGET, common.PrivilegeReadConfidential))
124
-			gv1.GET("/users/self/donor_info", Method(v1.UsersSelfDonorInfoGET, common.PrivilegeReadConfidential))
125
-			gv1.GET("/users/self/favourite_mode", Method(v1.UsersSelfFavouriteModeGET, common.PrivilegeReadConfidential))
126
-			gv1.GET("/users/self/settings", Method(v1.UsersSelfSettingsGET, common.PrivilegeReadConfidential))
127
-
128
-			// Write privilege required
129
-			gv1.POST("/friends/add", Method(v1.FriendsAddPOST, common.PrivilegeWrite))
130
-			gv1.POST("/friends/del", Method(v1.FriendsDelPOST, common.PrivilegeWrite))
131
-			gv1.POST("/users/self/settings", Method(v1.UsersSelfSettingsPOST, common.PrivilegeWrite))
132
-			gv1.POST("/users/self/userpage", Method(v1.UserSelfUserpagePOST, common.PrivilegeWrite))
133
-			gv1.POST("/beatmaps/rank_requests", Method(v1.BeatmapRankRequestsSubmitPOST, common.PrivilegeWrite))
134
-
135
-			// Admin: beatmap
136
-			gv1.POST("/beatmaps/set_status", Method(v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap))
137
-			gv1.GET("/beatmaps/ranked_frozen_full", Method(v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap))
138
-
139
-			// Admin: user managing
140
-			gv1.POST("/users/manage/set_allowed", Method(v1.UserManageSetAllowedPOST, common.PrivilegeManageUser))
141
-
142
-			// M E T A
143
-			// E     T    "wow thats so meta"
144
-			// T     E                  -- the one who said "wow thats so meta"
145
-			// A T E M
146
-			gv1.GET("/meta/restart", Method(v1.MetaRestartGET, common.PrivilegeAPIMeta))
147
-			gv1.GET("/meta/kill", Method(v1.MetaKillGET, common.PrivilegeAPIMeta))
148
-			gv1.GET("/meta/up_since", Method(v1.MetaUpSinceGET, common.PrivilegeAPIMeta))
149
-			gv1.GET("/meta/update", Method(v1.MetaUpdateGET, common.PrivilegeAPIMeta))
150
-
151
-			// User Managing + meta
152
-			gv1.POST("/tokens/fix_privileges", Method(v1.TokenFixPrivilegesPOST,
153
-				common.PrivilegeManageUser, common.PrivilegeAPIMeta))
154
-
155
-			// in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
156
-			gv1.GET("/get_user", PeppyMethod(peppy.GetUser))
157
-			gv1.GET("/get_match", PeppyMethod(peppy.GetMatch))
158
-			gv1.GET("/get_user_recent", PeppyMethod(peppy.GetUserRecent))
159
-			gv1.GET("/get_user_best", PeppyMethod(peppy.GetUserBest))
160
-			gv1.GET("/get_scores", PeppyMethod(peppy.GetScores))
161
-			gv1.GET("/get_beatmaps", PeppyMethod(peppy.GetBeatmap))
162
-		}
90
+	// v1 API
91
+	{
92
+		r.POST("/api/v1/tokens", Method(v1.TokenNewPOST))
93
+		r.POST("/api/v1/tokens/new", Method(v1.TokenNewPOST))
94
+		r.POST("/api/v1/tokens/self/delete", Method(v1.TokenSelfDeletePOST))
95
+
96
+		// Auth-free API endpoints (public data)
97
+		r.GET("/api/v1/ping", Method(v1.PingGET))
98
+		r.GET("/api/v1/surprise_me", Method(v1.SurpriseMeGET))
99
+		r.GET("/api/v1/doc", Method(v1.DocGET))
100
+		r.GET("/api/v1/doc/content", Method(v1.DocContentGET))
101
+		r.GET("/api/v1/doc/rules", Method(v1.DocRulesGET))
102
+		r.GET("/api/v1/users", Method(v1.UsersGET))
103
+		r.GET("/api/v1/users/whatid", Method(v1.UserWhatsTheIDGET))
104
+		r.GET("/api/v1/users/full", Method(v1.UserFullGET))
105
+		r.GET("/api/v1/users/userpage", Method(v1.UserUserpageGET))
106
+		r.GET("/api/v1/users/lookup", Method(v1.UserLookupGET))
107
+		r.GET("/api/v1/users/scores/best", Method(v1.UserScoresBestGET))
108
+		r.GET("/api/v1/users/scores/recent", Method(v1.UserScoresRecentGET))
109
+		r.GET("/api/v1/badges", Method(v1.BadgesGET))
110
+		r.GET("/api/v1/beatmaps", Method(v1.BeatmapGET))
111
+		r.GET("/api/v1/leaderboard", Method(v1.LeaderboardGET))
112
+		r.GET("/api/v1/tokens", Method(v1.TokenGET))
113
+		r.GET("/api/v1/users/self", Method(v1.UserSelfGET))
114
+		r.GET("/api/v1/tokens/self", Method(v1.TokenSelfGET))
115
+		r.GET("/api/v1/blog/posts", Method(v1.BlogPostsGET))
116
+		r.GET("/api/v1/scores", Method(v1.ScoresGET))
117
+		r.GET("/api/v1/beatmaps/rank_requests/status", Method(v1.BeatmapRankRequestsStatusGET))
118
+
119
+		// ReadConfidential privilege required
120
+		r.GET("/api/v1/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential))
121
+		r.GET("/api/v1/friends/with", Method(v1.FriendsWithGET, common.PrivilegeReadConfidential))
122
+		r.GET("/api/v1/users/self/donor_info", Method(v1.UsersSelfDonorInfoGET, common.PrivilegeReadConfidential))
123
+		r.GET("/api/v1/users/self/favourite_mode", Method(v1.UsersSelfFavouriteModeGET, common.PrivilegeReadConfidential))
124
+		r.GET("/api/v1/users/self/settings", Method(v1.UsersSelfSettingsGET, common.PrivilegeReadConfidential))
125
+
126
+		// Write privilege required
127
+		r.POST("/api/v1/friends/add", Method(v1.FriendsAddPOST, common.PrivilegeWrite))
128
+		r.POST("/api/v1/friends/del", Method(v1.FriendsDelPOST, common.PrivilegeWrite))
129
+		r.POST("/api/v1/users/self/settings", Method(v1.UsersSelfSettingsPOST, common.PrivilegeWrite))
130
+		r.POST("/api/v1/users/self/userpage", Method(v1.UserSelfUserpagePOST, common.PrivilegeWrite))
131
+		r.POST("/api/v1/beatmaps/rank_requests", Method(v1.BeatmapRankRequestsSubmitPOST, common.PrivilegeWrite))
132
+
133
+		// Admin: beatmap
134
+		r.POST("/api/v1/beatmaps/set_status", Method(v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap))
135
+		r.GET("/api/v1/beatmaps/ranked_frozen_full", Method(v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap))
136
+
137
+		// Admin: user managing
138
+		r.POST("/api/v1/users/manage/set_allowed", Method(v1.UserManageSetAllowedPOST, common.PrivilegeManageUser))
139
+
140
+		// M E T A
141
+		// E     T    "wow thats so meta"
142
+		// T     E                  -- the one who said "wow thats so meta"
143
+		// A T E M
144
+		r.GET("/api/v1/meta/restart", Method(v1.MetaRestartGET, common.PrivilegeAPIMeta))
145
+		r.GET("/api/v1/meta/kill", Method(v1.MetaKillGET, common.PrivilegeAPIMeta))
146
+		r.GET("/api/v1/meta/up_since", Method(v1.MetaUpSinceGET, common.PrivilegeAPIMeta))
147
+		r.GET("/api/v1/meta/update", Method(v1.MetaUpdateGET, common.PrivilegeAPIMeta))
148
+
149
+		// User Managing + meta
150
+		r.POST("/api/v1/tokens/fix_privileges", Method(v1.TokenFixPrivilegesPOST,
151
+			common.PrivilegeManageUser, common.PrivilegeAPIMeta))
152
+	}
163 153
 
164
-		api.GET("/status", internals.Status)
154
+	// in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
155
+	{
156
+		r.GET("/api/v1/get_user", PeppyMethod(peppy.GetUser))
157
+		r.GET("/api/v1/get_match", PeppyMethod(peppy.GetMatch))
158
+		r.GET("/api/v1/get_user_recent", PeppyMethod(peppy.GetUserRecent))
159
+		r.GET("/api/v1/get_user_best", PeppyMethod(peppy.GetUserBest))
160
+		r.GET("/api/v1/get_scores", PeppyMethod(peppy.GetScores))
161
+		r.GET("/api/v1/get_beatmaps", PeppyMethod(peppy.GetBeatmap))
165 162
 	}
166 163
 
167
-	r.NoRoute(v1.Handle404)
164
+	r.GET("/api/status", internals.Status)
165
+
166
+	r.NotFound = v1.Handle404
168 167
 
169 168
 	return r
170 169
 }

+ 12
- 5
app/v1/404.go View File

@@ -1,8 +1,10 @@
1 1
 package v1
2 2
 
3 3
 import (
4
+	"encoding/json"
5
+
6
+	"github.com/valyala/fasthttp"
4 7
 	"zxq.co/ripple/rippleapi/common"
5
-	"github.com/gin-gonic/gin"
6 8
 )
7 9
 
8 10
 type response404 struct {
@@ -11,12 +13,17 @@ type response404 struct {
11 13
 }
12 14
 
13 15
 // Handle404 handles requests with no implemented handlers.
14
-func Handle404(c *gin.Context) {
15
-	c.Header("X-Real-404", "yes")
16
-	c.IndentedJSON(404, response404{
16
+func Handle404(c *fasthttp.RequestCtx) {
17
+	c.Response.Header.Add("X-Real-404", "yes")
18
+	data, err := json.MarshalIndent(response404{
17 19
 		ResponseBase: common.ResponseBase{
18 20
 			Code: 404,
19 21
 		},
20 22
 		Cats: surpriseMe(),
21
-	})
23
+	}, "", "\t")
24
+	if err != nil {
25
+		panic(err)
26
+	}
27
+	c.SetStatusCode(404)
28
+	c.Write(data)
22 29
 }

+ 5
- 25
app/v1/beatmap.go View File

@@ -2,8 +2,6 @@ package v1
2 2
 
3 3
 import (
4 4
 	"database/sql"
5
-	"fmt"
6
-	"net/url"
7 5
 
8 6
 	"zxq.co/ripple/rippleapi/common"
9 7
 )
@@ -51,10 +49,10 @@ type beatmapSetStatusData struct {
51 49
 // the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16
52 50
 func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
53 51
 	var req beatmapSetStatusData
54
-	md.RequestData.Unmarshal(&req)
52
+	md.Unmarshal(&req)
55 53
 
56 54
 	var miss []string
57
-	if req.BeatmapsetID == 0 && req.BeatmapID == 0 {
55
+	if req.BeatmapsetID <= 0 && req.BeatmapID <= 0 {
58 56
 		miss = append(miss, "beatmapset_id or beatmap_id")
59 57
 	}
60 58
 	if len(miss) != 0 {
@@ -84,32 +82,14 @@ func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
84 82
 		SET ranked = ?, ranked_status_freezed = ?
85 83
 		WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param)
86 84
 
87
-	var x = make(map[string]interface{}, 1)
88
-	if req.BeatmapID != 0 {
89
-		x["bb"] = req.BeatmapID
85
+	if req.BeatmapID > 0 {
86
+		md.Ctx.Request.URI().QueryArgs().SetUint("bb", req.BeatmapID)
90 87
 	} else {
91
-		x["s"] = req.BeatmapsetID
88
+		md.Ctx.Request.URI().QueryArgs().SetUint("s", req.BeatmapsetID)
92 89
 	}
93
-	md.C.Request.URL = genURL(x)
94 90
 	return getMultipleBeatmaps(md)
95 91
 }
96 92
 
97
-func genURL(d map[string]interface{}) *url.URL {
98
-	var s string
99
-	for k, v := range d {
100
-		if s != "" {
101
-			s += "&"
102
-		}
103
-		s += k + "=" + url.QueryEscape(fmt.Sprintf("%v", v))
104
-	}
105
-	u := new(url.URL)
106
-	if len(d) == 0 {
107
-		return u
108
-	}
109
-	u.RawQuery = s
110
-	return u
111
-}
112
-
113 93
 // BeatmapGET retrieves a beatmap.
114 94
 func BeatmapGET(md common.MethodData) common.CodeMessager {
115 95
 	beatmapID := common.Int(md.Query("b"))

+ 1
- 4
app/v1/beatmap_requests.go View File

@@ -78,7 +78,7 @@ type submitRequestData struct {
78 78
 // BeatmapRankRequestsSubmitPOST submits a new beatmap for ranking approval.
79 79
 func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
80 80
 	var d submitRequestData
81
-	err := md.RequestData.Unmarshal(&d)
81
+	err := md.Unmarshal(&d)
82 82
 	if err != nil {
83 83
 		return ErrBadJSON
84 84
 	}
@@ -91,9 +91,6 @@ func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
91 91
 	if !limit.NonBlockingRequest("rankrequest:u:"+strconv.Itoa(md.ID()), 5) {
92 92
 		return common.SimpleResponse(429, "You may only try to request 5 beatmaps per minute.")
93 93
 	}
94
-	if !limit.NonBlockingRequest("rankrequest:ip:"+md.C.ClientIP(), 8) {
95
-		return common.SimpleResponse(429, "You may only try to request 8 beatmaps per minute from the same IP.")
96
-	}
97 94
 
98 95
 	// find out from BeatmapRankRequestsStatusGET if we can submit beatmaps.
99 96
 	statusRaw := BeatmapRankRequestsStatusGET(md)

+ 2
- 2
app/v1/friend.go View File

@@ -134,7 +134,7 @@ func FriendsAddPOST(md common.MethodData) common.CodeMessager {
134 134
 	var u struct {
135 135
 		User int `json:"user"`
136 136
 	}
137
-	md.RequestData.Unmarshal(&u)
137
+	md.Unmarshal(&u)
138 138
 	return addFriend(md, u.User)
139 139
 }
140 140
 
@@ -183,7 +183,7 @@ func FriendsDelPOST(md common.MethodData) common.CodeMessager {
183 183
 	var u struct {
184 184
 		User int `json:"user"`
185 185
 	}
186
-	md.RequestData.Unmarshal(&u)
186
+	md.Unmarshal(&u)
187 187
 	return delFriend(md, u.User)
188 188
 }
189 189
 

+ 1
- 1
app/v1/manage_user.go View File

@@ -14,7 +14,7 @@ type setAllowedData struct {
14 14
 // UserManageSetAllowedPOST allows to set the allowed status of an user.
15 15
 func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
16 16
 	data := setAllowedData{}
17
-	if err := md.RequestData.Unmarshal(&data); err != nil {
17
+	if err := md.Unmarshal(&data); err != nil {
18 18
 		return ErrBadJSON
19 19
 	}
20 20
 	if data.Allowed < 0 || data.Allowed > 2 {

+ 1
- 1
app/v1/self.go View File

@@ -62,7 +62,7 @@ type userSettingsData struct {
62 62
 // UsersSelfSettingsPOST allows to modify information about the current user.
63 63
 func UsersSelfSettingsPOST(md common.MethodData) common.CodeMessager {
64 64
 	var d userSettingsData
65
-	md.RequestData.Unmarshal(&d)
65
+	md.Unmarshal(&d)
66 66
 
67 67
 	// input sanitisation
68 68
 	*d.UsernameAKA = common.SanitiseString(*d.UsernameAKA)

+ 3
- 3
app/v1/token.go View File

@@ -8,10 +8,10 @@ import (
8 8
 
9 9
 	"github.com/jmoiron/sqlx"
10 10
 
11
+	"golang.org/x/crypto/bcrypt"
11 12
 	"zxq.co/ripple/rippleapi/common"
12 13
 	"zxq.co/ripple/rippleapi/limit"
13 14
 	"zxq.co/ripple/schiavolib"
14
-	"golang.org/x/crypto/bcrypt"
15 15
 )
16 16
 
17 17
 type tokenNewInData struct {
@@ -37,7 +37,7 @@ type tokenNewResponse struct {
37 37
 func TokenNewPOST(md common.MethodData) common.CodeMessager {
38 38
 	var r tokenNewResponse
39 39
 	data := tokenNewInData{}
40
-	err := md.RequestData.Unmarshal(&data)
40
+	err := md.Unmarshal(&data)
41 41
 	if err != nil {
42 42
 		return ErrBadJSON
43 43
 	}
@@ -80,7 +80,7 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
80 80
 	}
81 81
 	privileges := common.UserPrivileges(privilegesRaw)
82 82
 
83
-	if !limit.NonBlockingRequest(fmt.Sprintf("loginattempt:%d:%s", r.ID, md.C.ClientIP()), 5) {
83
+	if !limit.NonBlockingRequest(fmt.Sprintf("loginattempt:%d:%s", r.ID, md.ClientIP()), 5) {
84 84
 		return common.SimpleResponse(429, "You've made too many login attempts. Try again later.")
85 85
 	}
86 86
 

+ 18
- 13
app/v1/user.go View File

@@ -5,9 +5,9 @@ import (
5 5
 	"database/sql"
6 6
 	"strconv"
7 7
 	"strings"
8
+	"unicode"
8 9
 
9 10
 	"github.com/jmoiron/sqlx"
10
-
11 11
 	"zxq.co/ripple/ocl"
12 12
 	"zxq.co/ripple/rippleapi/common"
13 13
 )
@@ -71,8 +71,7 @@ type userPutsMultiUserData struct {
71 71
 }
72 72
 
73 73
 func userPutsMulti(md common.MethodData) common.CodeMessager {
74
-	q := md.C.Request.URL.Query()
75
-
74
+	pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
76 75
 	// query composition
77 76
 	wh := common.
78 77
 		Where("users.username_safe = ?", common.SafeUsername(md.Query("nname"))).
@@ -83,10 +82,10 @@ func userPutsMulti(md common.MethodData) common.CodeMessager {
83 82
 		Where("users_stats.country = ?", md.Query("country")).
84 83
 		Where("users_stats.username_aka = ?", md.Query("name_aka")).
85 84
 		Where("privileges_groups.name = ?", md.Query("privilege_group")).
86
-		In("users.id", q["ids"]...).
87
-		In("users.username_safe", safeUsernameBulk(q["names"])...).
88
-		In("users_stats.username_aka", q["names_aka"]...).
89
-		In("users_stats.country", q["countries"]...)
85
+		In("users.id", pm("ids")...).
86
+		In("users.username_safe", safeUsernameBulk(pm("names"))...).
87
+		In("users_stats.username_aka", pm("names_aka")...).
88
+		In("users_stats.country", pm("countries")...)
90 89
 
91 90
 	var extraJoin string
92 91
 	if md.Query("privilege_group") != "" {
@@ -130,13 +129,19 @@ func userPutsMulti(md common.MethodData) common.CodeMessager {
130 129
 
131 130
 // UserSelfGET is a shortcut for /users/id/self. (/users/self)
132 131
 func UserSelfGET(md common.MethodData) common.CodeMessager {
133
-	md.C.Request.URL.RawQuery = "id=self&" + md.C.Request.URL.RawQuery
132
+	md.Ctx.Request.URI().SetQueryString("id=self")
134 133
 	return UsersGET(md)
135 134
 }
136 135
 
137
-func safeUsernameBulk(us []string) []string {
138
-	for i, u := range us {
139
-		us[i] = common.SafeUsername(u)
136
+func safeUsernameBulk(us [][]byte) [][]byte {
137
+	for _, u := range us {
138
+		for idx, v := range u {
139
+			if v == ' ' {
140
+				u[idx] = '_'
141
+				continue
142
+			}
143
+			u[idx] = byte(unicode.ToLower(rune(v)))
144
+		}
140 145
 	}
141 146
 	return us
142 147
 }
@@ -341,7 +346,7 @@ func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
341 346
 	var d struct {
342 347
 		Data *string `json:"data"`
343 348
 	}
344
-	md.RequestData.Unmarshal(&d)
349
+	md.Unmarshal(&d)
345 350
 	if d.Data == nil {
346 351
 		return ErrMissingField("data")
347 352
 	}
@@ -350,7 +355,7 @@ func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
350 355
 	if err != nil {
351 356
 		md.Err(err)
352 357
 	}
353
-	md.C.Request.URL.RawQuery += "&id=" + strconv.Itoa(md.ID())
358
+	md.Ctx.URI().SetQueryString("id=self")
354 359
 	return UserUserpageGET(md)
355 360
 }
356 361
 

+ 29
- 0
common/conversions.go View File

@@ -0,0 +1,29 @@
1
+package common
2
+
3
+import (
4
+	"reflect"
5
+	"unsafe"
6
+)
7
+
8
+// b2s converts byte slice to a string without memory allocation.
9
+// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
10
+//
11
+// Note it may break if string and/or slice header will change
12
+// in the future go versions.
13
+func b2s(b []byte) string {
14
+	return *(*string)(unsafe.Pointer(&b))
15
+}
16
+
17
+// s2b converts string to a byte slice without memory allocation.
18
+//
19
+// Note it may break if string and/or slice header will change
20
+// in the future go versions.
21
+func s2b(s string) []byte {
22
+	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
23
+	bh := reflect.SliceHeader{
24
+		Data: sh.Data,
25
+		Len:  sh.Len,
26
+		Cap:  sh.Len,
27
+	}
28
+	return *(*[]byte)(unsafe.Pointer(&bh))
29
+}

+ 120
- 21
common/method_data.go View File

@@ -2,26 +2,132 @@ package common
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"fmt"
6
+	"strconv"
7
+	"strings"
5 8
 
6 9
 	"github.com/DataDog/datadog-go/statsd"
7
-	"github.com/gin-gonic/gin"
10
+	"github.com/getsentry/raven-go"
8 11
 	"github.com/jmoiron/sqlx"
12
+	"github.com/valyala/fasthttp"
9 13
 	"gopkg.in/redis.v5"
10 14
 )
11 15
 
16
+// RavenClient is the raven client to which report errors happening.
17
+// If nil, errors will just be fmt.Println'd
18
+var RavenClient *raven.Client
19
+
12 20
 // MethodData is a struct containing the data passed over to an API method.
13 21
 type MethodData struct {
14
-	User        Token
15
-	DB          *sqlx.DB
16
-	RequestData RequestData
17
-	C           *gin.Context
18
-	Doggo       *statsd.Client
19
-	R           *redis.Client
22
+	User  Token
23
+	DB    *sqlx.DB
24
+	Doggo *statsd.Client
25
+	R     *redis.Client
26
+	Ctx   *fasthttp.RequestCtx
27
+}
28
+
29
+// ClientIP implements a best effort algorithm to return the real client IP, it parses
30
+// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
31
+func (md MethodData) ClientIP() string {
32
+	clientIP := strings.TrimSpace(string(md.Ctx.Request.Header.Peek("X-Real-Ip")))
33
+	if len(clientIP) > 0 {
34
+		return clientIP
35
+	}
36
+	clientIP = string(md.Ctx.Request.Header.Peek("X-Forwarded-For"))
37
+	if index := strings.IndexByte(clientIP, ','); index >= 0 {
38
+		clientIP = clientIP[0:index]
39
+	}
40
+	clientIP = strings.TrimSpace(clientIP)
41
+	if len(clientIP) > 0 {
42
+		return clientIP
43
+	}
44
+	return md.Ctx.RemoteIP().String()
20 45
 }
21 46
 
22
-// Err logs an error into gin.
47
+// Err logs an error. If RavenClient is set, it will use the client to report
48
+// the error to sentry, otherwise it will just write the error to stdout.
23 49
 func (md MethodData) Err(err error) {
24
-	md.C.Error(err)
50
+	if RavenClient == nil {
51
+		fmt.Println("ERROR!!!!")
52
+		fmt.Println(err)
53
+		return
54
+	}
55
+
56
+	// Create stacktrace
57
+	st := raven.NewStacktrace(0, 3, []string{"zxq.co/ripple", "git.zxq.co/ripple"})
58
+
59
+	// Generate tags for error
60
+	tags := map[string]string{
61
+		"endpoint": b2s(md.Ctx.RequestURI()),
62
+		"token":    md.User.Value,
63
+	}
64
+
65
+	RavenClient.CaptureError(
66
+		err,
67
+		tags,
68
+		st,
69
+		generateRavenHTTP(md.Ctx),
70
+		&raven.User{
71
+			ID:       strconv.Itoa(md.User.UserID),
72
+			Username: md.User.Value,
73
+			IP:       md.Ctx.RemoteAddr().String(),
74
+		},
75
+	)
76
+}
77
+
78
+// Err for peppy API calls
79
+func Err(c *fasthttp.RequestCtx, err error) {
80
+	if RavenClient == nil {
81
+		fmt.Println("ERROR!!!!")
82
+		fmt.Println(err)
83
+		return
84
+	}
85
+
86
+	// Create stacktrace
87
+	st := raven.NewStacktrace(0, 3, []string{"zxq.co/ripple", "git.zxq.co/ripple"})
88
+
89
+	// Generate tags for error
90
+	tags := map[string]string{
91
+		"endpoint": b2s(c.RequestURI()),
92
+	}
93
+
94
+	RavenClient.CaptureError(
95
+		err,
96
+		tags,
97
+		st,
98
+		generateRavenHTTP(c),
99
+	)
100
+}
101
+
102
+func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
103
+	// build uri
104
+	uri := ctx.URI()
105
+	// safe to use b2s because a new string gets allocated eventually for
106
+	// concatenation
107
+	sURI := b2s(uri.Scheme()) + "://" + b2s(uri.Host()) + b2s(uri.Path())
108
+
109
+	// build header map
110
+	// using ctx.Request.Header.Len would mean calling .VisitAll two times
111
+	// which can be quite expensive since it means iterating over all the
112
+	// headers, so we give a rough estimate of the number of headers we expect
113
+	// to have
114
+	m := make(map[string]string, 16)
115
+	ctx.Request.Header.VisitAll(func(k, v []byte) {
116
+		// not using b2s because we mustn't keep references to the underlying
117
+		// k and v
118
+		m[string(k)] = string(v)
119
+	})
120
+
121
+	return &raven.Http{
122
+		URL: sURI,
123
+		// Not using b2s because raven sending is concurrent and may happen
124
+		// AFTER the request, meaning that values could potentially be replaced
125
+		// by new ones.
126
+		Method:  string(ctx.Method()),
127
+		Query:   string(uri.QueryString()),
128
+		Cookies: string(ctx.Request.Header.Peek("Cookie")),
129
+		Headers: m,
130
+	}
25 131
 }
26 132
 
27 133
 // ID retrieves the Token's owner user ID.
@@ -31,23 +137,16 @@ func (md MethodData) ID() int {
31 137
 
32 138
 // Query is shorthand for md.C.Query.
33 139
 func (md MethodData) Query(q string) string {
34
-	return md.C.Query(q)
140
+	return b2s(md.Ctx.QueryArgs().Peek(q))
35 141
 }
36 142
 
37 143
 // HasQuery returns true if the parameter is encountered in the querystring.
38 144
 // It returns true even if the parameter is "" (the case of ?param&etc=etc)
39 145
 func (md MethodData) HasQuery(q string) bool {
40
-	_, has := md.C.GetQuery(q)
41
-	return has
146
+	return md.Ctx.QueryArgs().Has(q)
42 147
 }
43 148
 
44
-// RequestData is the body of a request. It is wrapped into this type
45
-// to implement the Unmarshal function, which is just a shorthand to
46
-// json.Unmarshal.
47
-type RequestData []byte
48
-
49
-// Unmarshal json-decodes Requestdata into a value. Basically a
50
-// shorthand to json.Unmarshal.
51
-func (r RequestData) Unmarshal(into interface{}) error {
52
-	return json.Unmarshal([]byte(r), into)
149
+// Unmarshal unmarshals a request's JSON body into an interface.
150
+func (md MethodData) Unmarshal(into interface{}) error {
151
+	return json.Unmarshal(md.Ctx.PostBody(), into)
53 152
 }

+ 2
- 2
common/sort.go View File

@@ -19,8 +19,8 @@ func Sort(md MethodData, config SortConfiguration) string {
19 19
 		config.Table += "."
20 20
 	}
21 21
 	var sortBy string
22
-	for _, s := range md.C.Request.URL.Query()["sort"] {
23
-		sortParts := strings.Split(strings.ToLower(s), ",")
22
+	for _, s := range md.Ctx.Request.URI().QueryArgs().PeekMulti("sort") {
23
+		sortParts := strings.Split(strings.ToLower(b2s(s)), ",")
24 24
 		if contains(config.Allowed, sortParts[0]) {
25 25
 			if sortBy != "" {
26 26
 				sortBy += ", "

+ 4
- 4
common/where.go View File

@@ -51,15 +51,15 @@ func (w *WhereClause) And() *WhereClause {
51 51
 // initial is the initial part, e.g. "users.id".
52 52
 // Fields are the possible values.
53 53
 // Sample output: users.id IN ('1', '2', '3')
54
-func (w *WhereClause) In(initial string, fields ...string) *WhereClause {
54
+func (w *WhereClause) In(initial string, fields ...[]byte) *WhereClause {
55 55
 	if len(fields) == 0 {
56 56
 		return w
57 57
 	}
58 58
 	w.addWhere()
59 59
 	w.Clause += initial + " IN (" + generateQuestionMarks(len(fields)) + ")"
60
-	fieldsInterfaced := make([]interface{}, 0, len(fields))
61
-	for _, i := range fields {
62
-		fieldsInterfaced = append(fieldsInterfaced, interface{}(i))
60
+	fieldsInterfaced := make([]interface{}, len(fields))
61
+	for k, f := range fields {
62
+		fieldsInterfaced[k] = string(f)
63 63
 	}
64 64
 	w.Params = append(w.Params, fieldsInterfaced...)
65 65
 	return w

+ 1
- 1
main.go View File

@@ -62,5 +62,5 @@ func main() {
62 62
 
63 63
 	engine := app.Start(conf, db)
64 64
 
65
-	startuato(engine)
65
+	startuato(engine.Handler)
66 66
 }

+ 7
- 9
startuato_linux.go View File

@@ -3,19 +3,18 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"fmt"
6 7
 	"log"
7 8
 	"net"
8
-	"net/http"
9
-	"fmt"
10 9
 	"time"
11 10
 
12
-	"zxq.co/ripple/schiavolib"
13
-	"zxq.co/ripple/rippleapi/common"
14
-	"github.com/gin-gonic/gin"
15 11
 	"github.com/rcrowley/goagain"
12
+	"github.com/valyala/fasthttp"
13
+	"zxq.co/ripple/rippleapi/common"
14
+	"zxq.co/ripple/schiavolib"
16 15
 )
17 16
 
18
-func startuato(engine *gin.Engine) {
17
+func startuato(hn fasthttp.RequestHandler) {
19 18
 	conf, _ := common.Load()
20 19
 	// Inherit a net.Listener from our parent process or listen anew.
21 20
 	l, err := goagain.Listener()
@@ -35,13 +34,12 @@ func startuato(engine *gin.Engine) {
35 34
 		schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr()))
36 35
 
37 36
 		// Accept connections in a new goroutine.
38
-		go http.Serve(l, engine)
39
-
37
+		go fasthttp.Serve(l, hn)
40 38
 	} else {
41 39
 
42 40
 		// Resume accepting connections in a new goroutine.
43 41
 		schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr()))
44
-		go http.Serve(l, engine)
42
+		go fasthttp.Serve(l, hn)
45 43
 
46 44
 		// Kill the parent, now that the child has started successfully.
47 45
 		if err := goagain.Kill(); nil != err {

+ 10
- 7
startuato_windows.go View File

@@ -1,23 +1,26 @@
1 1
 // +build windows
2 2
 
3
+// The Ripple API on Windows is not officially supported and you're probably
4
+// gonna swear a lot if you intend to use it on Windows. Caveat emptor.
5
+
3 6
 package main
4 7
 
5 8
 import (
6
-	"net"
7 9
 	"log"
8
-	"net/http"
10
+	"net"
9 11
 
10
-	"github.com/gin-gonic/gin"
12
+	"github.com/valyala/fasthttp"
11 13
 	"zxq.co/ripple/rippleapi/common"
12 14
 )
13 15
 
14
-func startuato(engine *gin.Engine) {
16
+func startuato(hn fasthttp.RequestHandler) {
15 17
 	conf, _ := common.Load()
16 18
 	var (
17
-		l net.Listener
19
+		l   net.Listener
18 20
 		err error
19 21
 	)
20
-	// Listen on a TCP or a UNIX domain socket (TCP here).
22
+
23
+	// Listen on a TCP or a UNIX domain socket.
21 24
 	if conf.Unix {
22 25
 		l, err = net.Listen("unix", conf.ListenTo)
23 26
 	} else {
@@ -27,5 +30,5 @@ func startuato(engine *gin.Engine) {
27 30
 		log.Fatalln(err)
28 31
 	}
29 32
 
30
-	http.Serve(l, engine)
33
+	fasthttp.Serve(l, hn)
31 34
 }

Loading…
Cancel
Save