2020-01-02 02:12:45 +05:30
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2024-02-07 06:06:18 +05:30
|
|
|
"context"
|
2021-02-26 07:15:45 +05:30
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
2020-01-02 02:12:45 +05:30
|
|
|
"encoding/json"
|
2021-03-03 18:03:56 +05:30
|
|
|
"encoding/pem"
|
2024-02-07 18:54:41 +05:30
|
|
|
"fmt"
|
2020-01-02 02:12:45 +05:30
|
|
|
"net/http"
|
|
|
|
"strings"
|
2021-02-26 07:15:45 +05:30
|
|
|
"time"
|
2020-01-02 02:12:45 +05:30
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
2024-02-01 12:42:34 +05:30
|
|
|
"ely.by/chrly/internal/db"
|
|
|
|
"ely.by/chrly/internal/mojang"
|
|
|
|
"ely.by/chrly/internal/utils"
|
2020-01-02 02:12:45 +05:30
|
|
|
)
|
|
|
|
|
2021-02-26 07:15:45 +05:30
|
|
|
var timeNow = time.Now
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
type ProfilesProvider interface {
|
2024-02-07 06:06:18 +05:30
|
|
|
FindProfileByUsername(ctx context.Context, username string, allowProxy bool) (*db.Profile, error)
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
// TexturesSigner uses context because in the future we may separate this logic into a separate microservice
|
2021-02-26 07:15:45 +05:30
|
|
|
type TexturesSigner interface {
|
2024-02-13 06:38:42 +05:30
|
|
|
SignTextures(ctx context.Context, textures string) (string, error)
|
|
|
|
GetPublicKey(ctx context.Context) (*rsa.PublicKey, error)
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
2020-01-02 02:12:45 +05:30
|
|
|
type Skinsystem struct {
|
2024-01-30 13:35:04 +05:30
|
|
|
ProfilesProvider
|
|
|
|
TexturesSigner
|
2020-04-19 05:01:09 +05:30
|
|
|
TexturesExtraParamName string
|
|
|
|
TexturesExtraParamValue string
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) Handler() *mux.Router {
|
2020-01-02 02:12:45 +05:30
|
|
|
router := mux.NewRouter().StrictSlash(true)
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
router.HandleFunc("/skins/{username}", ctx.skinHandler).Methods(http.MethodGet)
|
2024-01-30 13:35:04 +05:30
|
|
|
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet)
|
|
|
|
// TODO: alias /capes/{username}?
|
2020-04-19 05:01:09 +05:30
|
|
|
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
|
|
|
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
2021-02-26 07:15:45 +05:30
|
|
|
router.HandleFunc("/profile/{username}", ctx.profileHandler).Methods(http.MethodGet)
|
2020-01-02 02:12:45 +05:30
|
|
|
// Legacy
|
2020-04-19 05:01:09 +05:30
|
|
|
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
|
|
|
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
2021-02-26 07:15:45 +05:30
|
|
|
// Utils
|
2021-03-03 18:03:56 +05:30
|
|
|
router.HandleFunc("/signature-verification-key.der", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet)
|
|
|
|
router.HandleFunc("/signature-verification-key.pem", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet)
|
2020-01-02 02:12:45 +05:30
|
|
|
|
|
|
|
return router
|
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
2024-02-07 06:06:18 +05:30
|
|
|
profile, err := ctx.ProfilesProvider.FindProfileByUsername(request.Context(), parseUsername(mux.Vars(request)["username"]), true)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
2020-04-30 00:24:40 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
if profile == nil || profile.SkinUrl == "" {
|
2020-01-02 02:12:45 +05:30
|
|
|
response.WriteHeader(http.StatusNotFound)
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
http.Redirect(response, request, profile.SkinUrl, http.StatusMovedPermanently)
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
2020-01-02 02:12:45 +05:30
|
|
|
username := request.URL.Query().Get("name")
|
|
|
|
if username == "" {
|
|
|
|
response.WriteHeader(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mux.Vars(request)["username"] = username
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
ctx.skinHandler(response, request)
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
2024-02-07 06:06:18 +05:30
|
|
|
profile, err := ctx.ProfilesProvider.FindProfileByUsername(request.Context(), parseUsername(mux.Vars(request)["username"]), true)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
if profile == nil || profile.CapeUrl == "" {
|
2020-04-30 00:24:40 +05:30
|
|
|
response.WriteHeader(http.StatusNotFound)
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
http.Redirect(response, request, profile.CapeUrl, http.StatusMovedPermanently)
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
2020-01-02 02:12:45 +05:30
|
|
|
username := request.URL.Query().Get("name")
|
|
|
|
if username == "" {
|
|
|
|
response.WriteHeader(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mux.Vars(request)["username"] = username
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
ctx.capeHandler(response, request)
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2020-04-19 05:01:09 +05:30
|
|
|
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
2024-02-07 06:06:18 +05:30
|
|
|
profile, err := ctx.ProfilesProvider.FindProfileByUsername(request.Context(), mux.Vars(request)["username"], true)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if profile == nil {
|
|
|
|
response.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
if profile.SkinUrl == "" && profile.CapeUrl == "" {
|
2021-02-26 07:15:45 +05:30
|
|
|
response.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
textures := texturesFromProfile(profile)
|
|
|
|
|
|
|
|
responseData, _ := json.Marshal(textures)
|
2021-02-26 07:15:45 +05:30
|
|
|
response.Header().Set("Content-Type", "application/json")
|
|
|
|
_, _ = response.Write(responseData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
2024-01-30 13:35:04 +05:30
|
|
|
profile, err := ctx.ProfilesProvider.FindProfileByUsername(
|
2024-02-07 06:06:18 +05:30
|
|
|
request.Context(),
|
2024-01-30 13:35:04 +05:30
|
|
|
mux.Vars(request)["username"],
|
|
|
|
getToBool(request.URL.Query().Get("proxy")),
|
|
|
|
)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
if profile == nil {
|
|
|
|
response.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if profile.MojangTextures == "" {
|
2021-02-26 07:15:45 +05:30
|
|
|
response.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
profileResponse := &mojang.ProfileResponse{
|
|
|
|
Id: profile.Uuid,
|
2021-02-26 07:15:45 +05:30
|
|
|
Name: profile.Username,
|
|
|
|
Props: []*mojang.Property{
|
|
|
|
{
|
|
|
|
Name: "textures",
|
|
|
|
Signature: profile.MojangSignature,
|
|
|
|
Value: profile.MojangTextures,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: ctx.TexturesExtraParamName,
|
|
|
|
Value: ctx.TexturesExtraParamValue,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
responseJson, _ := json.Marshal(profileResponse)
|
|
|
|
response.Header().Set("Content-Type", "application/json")
|
|
|
|
_, _ = response.Write(responseJson)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *http.Request) {
|
2024-02-07 06:06:18 +05:30
|
|
|
profile, err := ctx.ProfilesProvider.FindProfileByUsername(request.Context(), mux.Vars(request)["username"], true)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
if profile == nil {
|
2024-01-30 13:35:04 +05:30
|
|
|
response.WriteHeader(http.StatusNotFound)
|
2021-02-26 07:15:45 +05:30
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
texturesPropContent := &mojang.TexturesProp{
|
|
|
|
Timestamp: utils.UnixMillisecond(timeNow()),
|
2024-01-30 13:35:04 +05:30
|
|
|
ProfileID: profile.Uuid,
|
2021-02-26 07:15:45 +05:30
|
|
|
ProfileName: profile.Username,
|
2024-01-30 13:35:04 +05:30
|
|
|
Textures: texturesFromProfile(profile),
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
texturesPropValueJson, _ := json.Marshal(texturesPropContent)
|
|
|
|
texturesPropEncodedValue := base64.StdEncoding.EncodeToString(texturesPropValueJson)
|
|
|
|
|
|
|
|
texturesProp := &mojang.Property{
|
|
|
|
Name: "textures",
|
|
|
|
Value: texturesPropEncodedValue,
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
if request.URL.Query().Has("unsigned") && !getToBool(request.URL.Query().Get("unsigned")) {
|
2024-02-13 06:38:42 +05:30
|
|
|
signature, err := ctx.TexturesSigner.SignTextures(request.Context(), texturesProp.Value)
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
2024-02-07 18:54:41 +05:30
|
|
|
apiServerError(response, fmt.Errorf("unable to sign textures: %w", err))
|
2024-01-30 13:35:04 +05:30
|
|
|
return
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
texturesProp.Signature = signature
|
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
profileResponse := &mojang.ProfileResponse{
|
|
|
|
Id: profile.Uuid,
|
2021-02-26 07:15:45 +05:30
|
|
|
Name: profile.Username,
|
|
|
|
Props: []*mojang.Property{
|
|
|
|
texturesProp,
|
|
|
|
{
|
|
|
|
Name: ctx.TexturesExtraParamName,
|
|
|
|
Value: ctx.TexturesExtraParamValue,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
responseJson, _ := json.Marshal(profileResponse)
|
|
|
|
response.Header().Set("Content-Type", "application/json")
|
|
|
|
_, _ = response.Write(responseJson)
|
|
|
|
}
|
|
|
|
|
2021-02-27 07:07:59 +05:30
|
|
|
func (ctx *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWriter, request *http.Request) {
|
2024-02-13 06:38:42 +05:30
|
|
|
publicKey, err := ctx.TexturesSigner.GetPublicKey(request.Context())
|
2021-02-26 07:15:45 +05:30
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
asn1Bytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2021-03-03 18:03:56 +05:30
|
|
|
if strings.HasSuffix(request.URL.Path, ".pem") {
|
|
|
|
publicKeyBlock := pem.Block{
|
|
|
|
Type: "PUBLIC KEY",
|
|
|
|
Bytes: asn1Bytes,
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKeyPemBytes := pem.EncodeToMemory(&publicKeyBlock)
|
|
|
|
|
|
|
|
response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.pem\"")
|
|
|
|
_, _ = response.Write(publicKeyPemBytes)
|
|
|
|
} else {
|
|
|
|
response.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"")
|
|
|
|
_, _ = response.Write(asn1Bytes)
|
|
|
|
}
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
func parseUsername(username string) string {
|
|
|
|
return strings.TrimSuffix(username, ".png")
|
|
|
|
}
|
2020-01-02 02:12:45 +05:30
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
func getToBool(v string) bool {
|
|
|
|
return v == "true" || v == "1" || v == "yes"
|
|
|
|
}
|
2020-01-02 02:12:45 +05:30
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
func texturesFromProfile(profile *db.Profile) *mojang.TexturesResponse {
|
|
|
|
var skin *mojang.SkinTexturesResponse
|
|
|
|
if profile.SkinUrl != "" {
|
|
|
|
skin = &mojang.SkinTexturesResponse{
|
|
|
|
Url: profile.SkinUrl,
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
2024-01-30 13:35:04 +05:30
|
|
|
if profile.SkinModel != "" {
|
|
|
|
skin.Metadata = &mojang.SkinTexturesMetadata{
|
|
|
|
Model: profile.SkinModel,
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
2024-01-30 13:35:04 +05:30
|
|
|
}
|
2021-02-26 07:15:45 +05:30
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
var cape *mojang.CapeTexturesResponse
|
|
|
|
if profile.CapeUrl != "" {
|
|
|
|
cape = &mojang.CapeTexturesResponse{
|
|
|
|
Url: profile.CapeUrl,
|
2021-02-26 07:15:45 +05:30
|
|
|
}
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|
|
|
|
|
2024-01-30 13:35:04 +05:30
|
|
|
return &mojang.TexturesResponse{
|
|
|
|
Skin: skin,
|
|
|
|
Cape: cape,
|
|
|
|
}
|
2020-01-02 02:12:45 +05:30
|
|
|
}
|