mirror of https://github.com/mainnika/a-quest.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
4.1 KiB
180 lines
4.1 KiB
package lib
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
jwtgo "github.com/dgrijalva/jwt-go"
|
|
"github.com/go-redis/redis/v7"
|
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
|
"github.com/jackwhelpton/fasthttp-routing/v2/access"
|
|
"github.com/jackwhelpton/fasthttp-routing/v2/cors"
|
|
"github.com/jackwhelpton/fasthttp-routing/v2/fault"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
const (
|
|
URL = "/check"
|
|
URLHealthz = "/healthz"
|
|
)
|
|
|
|
type Api struct {
|
|
Base string
|
|
|
|
Alg string
|
|
Pub *ecdsa.PublicKey
|
|
Priv *ecdsa.PrivateKey
|
|
|
|
ScoresKey string
|
|
WinnersKey string
|
|
Redis *redis.Client
|
|
}
|
|
|
|
func (a *Api) GetHandler() fasthttp.RequestHandler {
|
|
|
|
crs := cors.Options{
|
|
AllowOrigins: "*",
|
|
AllowHeaders: "*",
|
|
AllowMethods: "*",
|
|
AllowCredentials: true,
|
|
}
|
|
|
|
router := routing.New()
|
|
router.Use(
|
|
access.Logger(log.Debugf),
|
|
cors.Handler(crs),
|
|
fault.PanicHandler(log.Warnf),
|
|
)
|
|
|
|
base := strings.TrimSuffix(a.Base, "/")
|
|
api := router.Group(base)
|
|
|
|
api.Post(URL, a.checkScore)
|
|
api.Get(URLHealthz, a.healthCheck)
|
|
|
|
return router.HandleRequest
|
|
}
|
|
|
|
func (a *Api) healthCheck(ctx *routing.Context) (err error) {
|
|
|
|
_, err = a.Redis.Ping().Result()
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
ctx.Response.Header.Set("Content-Type", "application/json")
|
|
_, err = ctx.WriteString("{\"health\":\"ok\"}")
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (a *Api) checkScore(ctx *routing.Context) (err error) {
|
|
|
|
type Score struct {
|
|
Key string `json:"key"`
|
|
Score int64 `json:"score"`
|
|
}
|
|
|
|
type Response struct {
|
|
Top []redis.Z `json:"top"`
|
|
Msg []string `json:"msg"`
|
|
}
|
|
|
|
now := time.Now()
|
|
body := ctx.Request.Body()
|
|
claims := &jwtgo.StandardClaims{}
|
|
score := &Score{}
|
|
|
|
err = json.Unmarshal(body, score)
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
}
|
|
|
|
_, err = jwtgo.ParseWithClaims(score.Key, claims, a.getKey)
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusForbidden, err.Error())
|
|
}
|
|
|
|
nowunix := fmt.Sprintf("%d", now.UnixNano()) // 1578057345996508551
|
|
member := fmt.Sprintf("%s:%s", nowunix, claims.Subject)
|
|
|
|
if len(nowunix) != 19 {
|
|
panic(errors.New("o rly"))
|
|
}
|
|
|
|
_, err = a.Redis.ZAddNX(a.ScoresKey, &redis.Z{Score: float64(score.Score), Member: member}).Result()
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
zz, err := a.Redis.ZRevRangeWithScores(a.ScoresKey, 0, 2).Result()
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
zz = append(zz, redis.Z{
|
|
Score: 201920192019,
|
|
Member: fmt.Sprintf("%d:%s", time.Unix(claims.IssuedAt, now.Unix()).UnixNano(), claims.Issuer),
|
|
})
|
|
|
|
sort.Slice(zz, func(i, j int) bool {
|
|
return zz[i].Score > zz[j].Score
|
|
})
|
|
|
|
resp := &Response{Top: zz}
|
|
|
|
if int64(zz[0].Score) > score.Score {
|
|
resp.Msg = []string{
|
|
`what a game! but try harder next time`,
|
|
}
|
|
} else {
|
|
resp.Msg = []string{
|
|
`congratulations! you are the winner!`,
|
|
`to enter the next level use:`,
|
|
`nc 1451e73305328869824eda4c81a75cb5.mehrweg.ga 31337`,
|
|
`send your existing key to the server to let the next level run`,
|
|
}
|
|
|
|
_, err = a.Redis.SAdd(a.WinnersKey, claims.Id).Result()
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
}
|
|
|
|
body, err = json.Marshal(resp)
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
ctx.Response.Header.Set("Content-Type", "application/json")
|
|
err = ctx.Write(body)
|
|
if err != nil {
|
|
return routing.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (a *Api) getPrivKey(t *jwtgo.Token) (interface{}, error) {
|
|
if _, ok := t.Method.(*jwtgo.SigningMethodECDSA); !ok {
|
|
return nil, errors.New("unexpected signing method")
|
|
}
|
|
return a.Priv, nil
|
|
}
|
|
|
|
func (a *Api) getKey(t *jwtgo.Token) (interface{}, error) {
|
|
if _, ok := t.Method.(*jwtgo.SigningMethodECDSA); !ok {
|
|
return nil, errors.New("unexpected signing method")
|
|
}
|
|
return a.Pub, nil
|
|
}
|
|
|