mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 02:46:03 -04:00
271 lines
7.3 KiB
Go
271 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/samber/mo"
|
|
"gorm.io/gorm"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type sessionJoinRequest struct {
|
|
AccessToken string `json:"accessToken"`
|
|
SelectedProfile string `json:"selectedProfile"`
|
|
ServerID string `json:"serverId"`
|
|
}
|
|
|
|
// /session/minecraft/join
|
|
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_Encryption#Client
|
|
func SessionJoin(app *App) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
req := new(sessionJoinRequest)
|
|
if err := c.Bind(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
client := app.GetClient(req.AccessToken, StalePolicyDeny)
|
|
if client == nil {
|
|
return &YggdrasilError{Code: http.StatusForbidden, Error_: mo.Some("ForbiddenOperationException")}
|
|
}
|
|
|
|
player := client.Player
|
|
|
|
player.ServerID = MakeNullString(&req.ServerID)
|
|
result := app.DB.Save(&player)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// /game/joinserver.jsp
|
|
func SessionJoinServer(app *App) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
playerName := c.QueryParam("user")
|
|
sessionID := c.QueryParam("sessionId")
|
|
serverID := c.QueryParam("serverId")
|
|
|
|
// If any parameters are missing, return NO
|
|
if playerName == "" || sessionID == "" || serverID == "" {
|
|
return c.String(http.StatusOK, "Bad login")
|
|
|
|
}
|
|
|
|
// Parse sessionId. It has the form:
|
|
// token:<accessToken>:<player UUID>
|
|
split := strings.Split(sessionID, ":")
|
|
if len(split) != 3 || split[0] != "token" {
|
|
return c.String(http.StatusOK, "Bad login")
|
|
}
|
|
accessToken := split[1]
|
|
id := split[2]
|
|
|
|
// Is the accessToken valid?
|
|
client := app.GetClient(accessToken, StalePolicyDeny)
|
|
if client == nil {
|
|
return c.String(http.StatusOK, "Bad login")
|
|
}
|
|
|
|
// If the player name corresponding to the access token doesn't match
|
|
// the `user` param from the request, return NO
|
|
player := client.Player
|
|
if player.Name != playerName {
|
|
return c.String(http.StatusOK, "Bad login")
|
|
}
|
|
// If the player's UUID doesn't match the UUID in the sessionId, return
|
|
// NO
|
|
playerID, err := UUIDToID(player.UUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if playerID != id {
|
|
return c.String(http.StatusOK, "Bad login")
|
|
}
|
|
|
|
player.ServerID = MakeNullString(&serverID)
|
|
result := app.DB.Save(&player)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
|
|
return c.String(http.StatusOK, "OK")
|
|
}
|
|
}
|
|
|
|
func fullProfile(app *App, user *User, player *Player, uuid string, sign bool, fromAuthlibInjector bool) (SessionProfileResponse, error) {
|
|
id, err := UUIDToID(uuid)
|
|
if err != nil {
|
|
return SessionProfileResponse{}, err
|
|
}
|
|
|
|
texturesProperty, err := app.GetSkinTexturesProperty(player, sign)
|
|
if err != nil {
|
|
return SessionProfileResponse{}, err
|
|
}
|
|
|
|
properties := []SessionProfileProperty{texturesProperty}
|
|
|
|
if fromAuthlibInjector {
|
|
var uploadableTextures []string
|
|
if app.Config.AllowSkins || user.IsAdmin {
|
|
uploadableTextures = append(uploadableTextures, "skin")
|
|
}
|
|
if app.Config.AllowCapes || user.IsAdmin {
|
|
uploadableTextures = append(uploadableTextures, "cape")
|
|
}
|
|
properties = append(properties, SessionProfileProperty{
|
|
Name: "uploadableTextures",
|
|
Value: strings.Join(uploadableTextures, ","),
|
|
})
|
|
}
|
|
|
|
return SessionProfileResponse{
|
|
ID: id,
|
|
Name: player.Name,
|
|
Properties: properties,
|
|
}, nil
|
|
}
|
|
|
|
func (app *App) hasJoined(c *echo.Context, playerName string, serverID string, legacy bool) error {
|
|
var player Player
|
|
result := app.DB.Preload("User").First(&player, "name = ?", playerName)
|
|
user := player.User
|
|
// If the error isn't "not found", throw.
|
|
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return result.Error
|
|
}
|
|
|
|
if result.Error != nil || !player.ServerID.Valid || serverID != player.ServerID.String {
|
|
for _, fallbackAPIServer := range app.FallbackAPIServers {
|
|
if !fallbackAPIServer.Config.EnableAuthentication {
|
|
continue
|
|
}
|
|
if fallbackAPIServer.Config.DenyUnknownUsers && result.Error != nil {
|
|
// If DenyUnknownUsers is enabled and the player name is
|
|
// not known, don't query the fallback server.
|
|
continue
|
|
}
|
|
base, err := url.Parse(fallbackAPIServer.Config.SessionURL)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
|
|
base.Path += "/session/minecraft/hasJoined"
|
|
params := url.Values{}
|
|
params.Add("username", playerName)
|
|
params.Add("serverId", serverID)
|
|
base.RawQuery = params.Encode()
|
|
|
|
res, err := MakeHTTPClient().Get(base.String())
|
|
if err != nil {
|
|
log.Printf("Received invalid response from fallback API server at %s\n", base.String())
|
|
continue
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode == http.StatusOK {
|
|
if legacy {
|
|
return (*c).String(http.StatusOK, "YES")
|
|
} else {
|
|
return (*c).Stream(http.StatusOK, res.Header.Get("Content-Type"), res.Body)
|
|
}
|
|
}
|
|
}
|
|
|
|
if legacy {
|
|
return (*c).String(http.StatusMethodNotAllowed, "NO")
|
|
} else {
|
|
return (*c).NoContent(http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
if legacy {
|
|
return (*c).String(http.StatusOK, "YES")
|
|
}
|
|
|
|
profile, err := fullProfile(app, &user, &player, player.UUID, true, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return (*c).JSON(http.StatusOK, profile)
|
|
}
|
|
|
|
// /session/minecraft/hasJoined
|
|
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_Encryption#Server
|
|
func SessionHasJoined(app *App) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
playerName := c.QueryParam("username")
|
|
serverID := c.QueryParam("serverId")
|
|
return app.hasJoined(&c, playerName, serverID, false)
|
|
}
|
|
}
|
|
|
|
// /game/checkserver.jsp
|
|
func SessionCheckServer(app *App) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
playerName := c.QueryParam("user")
|
|
serverID := c.QueryParam("serverId")
|
|
return app.hasJoined(&c, playerName, serverID, true)
|
|
}
|
|
}
|
|
|
|
// /session/minecraft/profile/:id
|
|
// https://minecraft.wiki/w/Mojang_API#Query_player's_skin_and_cape
|
|
func SessionProfile(app *App, fromAuthlibInjector bool) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
id := c.Param("id")
|
|
uuid_, err := ParseUUID(id)
|
|
if err != nil {
|
|
return &YggdrasilError{
|
|
Code: http.StatusBadRequest,
|
|
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", id)),
|
|
}
|
|
}
|
|
|
|
player, user, err := app.FindPlayerByUUIDOrOfflineUUID(uuid_)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if player == nil {
|
|
for _, fallbackAPIServer := range app.FallbackAPIServers {
|
|
reqURL := fallbackAPIServer.Config.SessionURL + "/session/minecraft/profile/" + url.PathEscape(uuid_)
|
|
res, err := app.CachedGet(reqURL+"?unsigned=false", fallbackAPIServer.Config.CacheTTLSeconds)
|
|
if err != nil {
|
|
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
|
|
continue
|
|
}
|
|
|
|
if res.StatusCode == http.StatusOK {
|
|
return c.Blob(http.StatusOK, "application/json", res.BodyBytes)
|
|
}
|
|
}
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
sign := c.QueryParam("unsigned") == "false"
|
|
profile, err := fullProfile(app, user, player, uuid_, sign, fromAuthlibInjector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, profile)
|
|
}
|
|
}
|
|
|
|
// /blockedservers
|
|
// https://minecraft.wiki/w/Mojang_API#Query_blocked_server_list
|
|
func SessionBlockedServers(app *App) func(c echo.Context) error {
|
|
return func(c echo.Context) error {
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
}
|