mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	
		
			
				
	
	
		
			446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package http
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/gorilla/mux"
 | 
						|
	"github.com/thedevsaddam/govalidator"
 | 
						|
 | 
						|
	"github.com/elyby/chrly/api/mojang"
 | 
						|
	"github.com/elyby/chrly/model"
 | 
						|
)
 | 
						|
 | 
						|
//noinspection GoSnakeCaseUsage
 | 
						|
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
 | 
						|
 | 
						|
var regexUuidAny = regexp.MustCompile(UUID_ANY)
 | 
						|
 | 
						|
func init() {
 | 
						|
	govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
 | 
						|
		if message == "" {
 | 
						|
			message = "Skin uploading is temporary unavailable"
 | 
						|
		}
 | 
						|
 | 
						|
		return errors.New(message)
 | 
						|
	})
 | 
						|
 | 
						|
	// Add ability to validate any possible uuid form
 | 
						|
	govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
 | 
						|
		str := value.(string)
 | 
						|
		if !regexUuidAny.MatchString(str) {
 | 
						|
			if message == "" {
 | 
						|
				message = fmt.Sprintf("The %s field must contain valid UUID", field)
 | 
						|
			}
 | 
						|
 | 
						|
			return errors.New(message)
 | 
						|
		}
 | 
						|
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
type SkinsRepository interface {
 | 
						|
	FindByUsername(username string) (*model.Skin, error)
 | 
						|
	FindByUserId(id int) (*model.Skin, error)
 | 
						|
	Save(skin *model.Skin) error
 | 
						|
	RemoveByUserId(id int) error
 | 
						|
	RemoveByUsername(username string) error
 | 
						|
}
 | 
						|
 | 
						|
type CapesRepository interface {
 | 
						|
	FindByUsername(username string) (*model.Cape, error)
 | 
						|
}
 | 
						|
 | 
						|
type SkinNotFoundError struct {
 | 
						|
	Who string
 | 
						|
}
 | 
						|
 | 
						|
func (e SkinNotFoundError) Error() string {
 | 
						|
	return "skin data not found"
 | 
						|
}
 | 
						|
 | 
						|
type CapeNotFoundError struct {
 | 
						|
	Who string
 | 
						|
}
 | 
						|
 | 
						|
func (e CapeNotFoundError) Error() string {
 | 
						|
	return "cape file not found"
 | 
						|
}
 | 
						|
 | 
						|
type MojangTexturesProvider interface {
 | 
						|
	GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
 | 
						|
}
 | 
						|
 | 
						|
type Skinsystem struct {
 | 
						|
	Emitter
 | 
						|
	TexturesExtraParamName  string
 | 
						|
	TexturesExtraParamValue string
 | 
						|
	SkinsRepo               SkinsRepository
 | 
						|
	CapesRepo               CapesRepository
 | 
						|
	MojangTexturesProvider  MojangTexturesProvider
 | 
						|
	Authenticator           Authenticator
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) CreateHandler() *mux.Router {
 | 
						|
	requestEventsMiddleware := CreateRequestEventsMiddleware(ctx.Emitter, "skinsystem")
 | 
						|
 | 
						|
	router := mux.NewRouter().StrictSlash(true)
 | 
						|
	router.Use(requestEventsMiddleware)
 | 
						|
 | 
						|
	router.HandleFunc("/skins/{username}", ctx.Skin).Methods(http.MethodGet)
 | 
						|
	router.HandleFunc("/cloaks/{username}", ctx.Cape).Methods(http.MethodGet).Name("cloaks")
 | 
						|
	router.HandleFunc("/textures/{username}", ctx.Textures).Methods(http.MethodGet)
 | 
						|
	router.HandleFunc("/textures/signed/{username}", ctx.SignedTextures).Methods(http.MethodGet)
 | 
						|
	// Legacy
 | 
						|
	router.HandleFunc("/skins", ctx.SkinGET).Methods(http.MethodGet)
 | 
						|
	router.HandleFunc("/cloaks", ctx.CapeGET).Methods(http.MethodGet)
 | 
						|
	// API
 | 
						|
	apiRouter := router.PathPrefix("/api").Subrouter()
 | 
						|
	apiRouter.Use(CreateAuthenticationMiddleware(ctx.Authenticator))
 | 
						|
	apiRouter.HandleFunc("/skins", ctx.PostSkin).Methods(http.MethodPost)
 | 
						|
	apiRouter.HandleFunc("/skins/id:{id:[0-9]+}", ctx.DeleteSkinByUserId).Methods(http.MethodDelete)
 | 
						|
	apiRouter.HandleFunc("/skins/{username}", ctx.DeleteSkinByUsername).Methods(http.MethodDelete)
 | 
						|
	// 404
 | 
						|
	// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
 | 
						|
	// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
 | 
						|
	router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFound))
 | 
						|
 | 
						|
	return router
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := parseUsername(mux.Vars(request)["username"])
 | 
						|
	rec, err := ctx.SkinsRepo.FindByUsername(username)
 | 
						|
	if err == nil && rec.SkinId != 0 {
 | 
						|
		http.Redirect(response, request, rec.Url, 301)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
						|
	if err != nil || mojangTextures == nil {
 | 
						|
		response.WriteHeader(http.StatusNotFound)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	texturesProp := mojangTextures.DecodeTextures()
 | 
						|
	skin := texturesProp.Textures.Skin
 | 
						|
	if skin == nil {
 | 
						|
		response.WriteHeader(http.StatusNotFound)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	http.Redirect(response, request, skin.Url, 301)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := request.URL.Query().Get("name")
 | 
						|
	if username == "" {
 | 
						|
		response.WriteHeader(http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mux.Vars(request)["username"] = username
 | 
						|
	mux.Vars(request)["converted"] = "1"
 | 
						|
 | 
						|
	ctx.Skin(response, request)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := parseUsername(mux.Vars(request)["username"])
 | 
						|
	rec, err := ctx.CapesRepo.FindByUsername(username)
 | 
						|
	if err == nil {
 | 
						|
		request.Header.Set("Content-Type", "image/png")
 | 
						|
		_, _ = io.Copy(response, rec.File)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
						|
	if err != nil || mojangTextures == nil {
 | 
						|
		response.WriteHeader(http.StatusNotFound)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	texturesProp := mojangTextures.DecodeTextures()
 | 
						|
	cape := texturesProp.Textures.Cape
 | 
						|
	if cape == nil {
 | 
						|
		response.WriteHeader(http.StatusNotFound)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	http.Redirect(response, request, cape.Url, 301)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := request.URL.Query().Get("name")
 | 
						|
	if username == "" {
 | 
						|
		response.WriteHeader(http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mux.Vars(request)["username"] = username
 | 
						|
	mux.Vars(request)["converted"] = "1"
 | 
						|
 | 
						|
	ctx.Cape(response, request)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := parseUsername(mux.Vars(request)["username"])
 | 
						|
 | 
						|
	var textures *mojang.TexturesResponse
 | 
						|
	skin, skinErr := ctx.SkinsRepo.FindByUsername(username)
 | 
						|
	_, capeErr := ctx.CapesRepo.FindByUsername(username)
 | 
						|
	if (skinErr == nil && skin.SkinId != 0) || capeErr == nil {
 | 
						|
		textures = &mojang.TexturesResponse{}
 | 
						|
 | 
						|
		if skinErr == nil && skin.SkinId != 0 {
 | 
						|
			skinTextures := &mojang.SkinTexturesResponse{
 | 
						|
				Url: skin.Url,
 | 
						|
			}
 | 
						|
 | 
						|
			if skin.IsSlim {
 | 
						|
				skinTextures.Metadata = &mojang.SkinTexturesMetadata{
 | 
						|
					Model: "slim",
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			textures.Skin = skinTextures
 | 
						|
		}
 | 
						|
 | 
						|
		if capeErr == nil {
 | 
						|
			textures.Cape = &mojang.CapeTexturesResponse{
 | 
						|
				Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username,
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
						|
		if err != nil || mojangTextures == nil {
 | 
						|
			response.WriteHeader(http.StatusNoContent)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		texturesProp := mojangTextures.DecodeTextures()
 | 
						|
		if texturesProp == nil {
 | 
						|
			ctx.Emit("skinsystem:error", errors.New("unable to find textures property"))
 | 
						|
			apiServerError(response)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		textures = texturesProp.Textures
 | 
						|
	}
 | 
						|
 | 
						|
	responseData, _ := json.Marshal(textures)
 | 
						|
	response.Header().Set("Content-Type", "application/json")
 | 
						|
	_, _ = response.Write(responseData)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *http.Request) {
 | 
						|
	username := parseUsername(mux.Vars(request)["username"])
 | 
						|
 | 
						|
	var responseData *mojang.SignedTexturesResponse
 | 
						|
 | 
						|
	rec, err := ctx.SkinsRepo.FindByUsername(username)
 | 
						|
	if err == nil && rec.SkinId != 0 && rec.MojangTextures != "" {
 | 
						|
		responseData = &mojang.SignedTexturesResponse{
 | 
						|
			Id:   strings.Replace(rec.Uuid, "-", "", -1),
 | 
						|
			Name: rec.Username,
 | 
						|
			Props: []*mojang.Property{
 | 
						|
				{
 | 
						|
					Name:      "textures",
 | 
						|
					Signature: rec.MojangSignature,
 | 
						|
					Value:     rec.MojangTextures,
 | 
						|
				},
 | 
						|
			},
 | 
						|
		}
 | 
						|
	} else if request.URL.Query().Get("proxy") != "" {
 | 
						|
		mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
 | 
						|
		if err == nil && mojangTextures != nil {
 | 
						|
			responseData = mojangTextures
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if responseData == nil {
 | 
						|
		response.WriteHeader(http.StatusNoContent)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	responseData.Props = append(responseData.Props, &mojang.Property{
 | 
						|
		Name:  getStringOrDefault(ctx.TexturesExtraParamName, "chrly"),
 | 
						|
		Value: getStringOrDefault(ctx.TexturesExtraParamValue, "how do you tame a horse in Minecraft?"),
 | 
						|
	})
 | 
						|
 | 
						|
	responseJson, _ := json.Marshal(responseData)
 | 
						|
	response.Header().Set("Content-Type", "application/json")
 | 
						|
	_, _ = response.Write(responseJson)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) PostSkin(resp http.ResponseWriter, req *http.Request) {
 | 
						|
	validationErrors := validatePostSkinRequest(req)
 | 
						|
	if validationErrors != nil {
 | 
						|
		apiBadRequest(resp, validationErrors)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
 | 
						|
	username := req.Form.Get("username")
 | 
						|
 | 
						|
	record, err := findIdentity(ctx.SkinsRepo, identityId, username)
 | 
						|
	if err != nil {
 | 
						|
		ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
 | 
						|
		apiServerError(resp)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
 | 
						|
	is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
 | 
						|
	isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
 | 
						|
 | 
						|
	record.Uuid = req.Form.Get("uuid")
 | 
						|
	record.SkinId = skinId
 | 
						|
	record.Is1_8 = is18
 | 
						|
	record.IsSlim = isSlim
 | 
						|
	record.Url = req.Form.Get("url")
 | 
						|
	record.MojangTextures = req.Form.Get("mojangTextures")
 | 
						|
	record.MojangSignature = req.Form.Get("mojangSignature")
 | 
						|
 | 
						|
	err = ctx.SkinsRepo.Save(record)
 | 
						|
	if err != nil {
 | 
						|
		ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
 | 
						|
		apiServerError(resp)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	resp.WriteHeader(http.StatusCreated)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) {
 | 
						|
	id, _ := strconv.Atoi(mux.Vars(req)["id"])
 | 
						|
	skin, err := ctx.SkinsRepo.FindByUserId(id)
 | 
						|
	ctx.deleteSkin(skin, err, resp)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) {
 | 
						|
	username := mux.Vars(req)["username"]
 | 
						|
	skin, err := ctx.SkinsRepo.FindByUsername(username)
 | 
						|
	ctx.deleteSkin(skin, err, resp)
 | 
						|
}
 | 
						|
 | 
						|
func (ctx *Skinsystem) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
 | 
						|
	if err != nil {
 | 
						|
		if _, ok := err.(*SkinNotFoundError); ok {
 | 
						|
			apiNotFound(resp, "Cannot find record for the requested identifier")
 | 
						|
		} else {
 | 
						|
			ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
 | 
						|
			apiServerError(resp)
 | 
						|
		}
 | 
						|
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	err = ctx.SkinsRepo.RemoveByUserId(skin.UserId)
 | 
						|
	if err != nil {
 | 
						|
		ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
 | 
						|
		apiServerError(resp)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	resp.WriteHeader(http.StatusNoContent)
 | 
						|
}
 | 
						|
 | 
						|
func validatePostSkinRequest(request *http.Request) map[string][]string {
 | 
						|
	const maxMultipartMemory int64 = 32 << 20
 | 
						|
	const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
 | 
						|
 | 
						|
	_ = request.ParseMultipartForm(maxMultipartMemory)
 | 
						|
 | 
						|
	validationRules := govalidator.MapData{
 | 
						|
		"identityId": {"required", "numeric", "min:1"},
 | 
						|
		"username":   {"required"},
 | 
						|
		"uuid":       {"required", "uuid_any"},
 | 
						|
		"skinId":     {"required", "numeric", "min:1"},
 | 
						|
		"url":        {"url"},
 | 
						|
		"file:skin":  {"ext:png", "size:24576", "mime:image/png"},
 | 
						|
		"is1_8":      {"bool"},
 | 
						|
		"isSlim":     {"bool"},
 | 
						|
	}
 | 
						|
 | 
						|
	shouldAppendSkinRequiredError := false
 | 
						|
	url := request.Form.Get("url")
 | 
						|
	_, _, skinErr := request.FormFile("skin")
 | 
						|
	if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
 | 
						|
		shouldAppendSkinRequiredError = true
 | 
						|
	} else if skinErr == nil {
 | 
						|
		validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
 | 
						|
	} else if url != "" {
 | 
						|
		validationRules["is1_8"] = append(validationRules["is1_8"], "required")
 | 
						|
		validationRules["isSlim"] = append(validationRules["isSlim"], "required")
 | 
						|
	}
 | 
						|
 | 
						|
	mojangTextures := request.Form.Get("mojangTextures")
 | 
						|
	if mojangTextures != "" {
 | 
						|
		validationRules["mojangSignature"] = []string{"required"}
 | 
						|
	}
 | 
						|
 | 
						|
	validator := govalidator.New(govalidator.Options{
 | 
						|
		Request:         request,
 | 
						|
		Rules:           validationRules,
 | 
						|
		RequiredDefault: false,
 | 
						|
		FormSize:        maxMultipartMemory,
 | 
						|
	})
 | 
						|
	validationResults := validator.Validate()
 | 
						|
	if shouldAppendSkinRequiredError {
 | 
						|
		validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
 | 
						|
		validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(validationResults) != 0 {
 | 
						|
		return validationResults
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func findIdentity(repo SkinsRepository, identityId int, username string) (*model.Skin, error) {
 | 
						|
	var record *model.Skin
 | 
						|
	record, err := repo.FindByUserId(identityId)
 | 
						|
	if err != nil {
 | 
						|
		if _, isSkinNotFound := err.(*SkinNotFoundError); !isSkinNotFound {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		record, err = repo.FindByUsername(username)
 | 
						|
		if err == nil {
 | 
						|
			_ = repo.RemoveByUsername(username)
 | 
						|
			record.UserId = identityId
 | 
						|
		} else {
 | 
						|
			record = &model.Skin{
 | 
						|
				UserId:   identityId,
 | 
						|
				Username: username,
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else if record.Username != username {
 | 
						|
		_ = repo.RemoveByUserId(identityId)
 | 
						|
		record.Username = username
 | 
						|
	}
 | 
						|
 | 
						|
	return record, nil
 | 
						|
}
 | 
						|
 | 
						|
func parseUsername(username string) string {
 | 
						|
	return strings.TrimSuffix(username, ".png")
 | 
						|
}
 | 
						|
 | 
						|
func getStringOrDefault(value string, def string) string {
 | 
						|
	if value != "" {
 | 
						|
		return value
 | 
						|
	}
 | 
						|
 | 
						|
	return def
 | 
						|
}
 |