mirror of
https://github.com/Mezeporta/Erupe.git
synced 2025-12-16 08:55:31 +01:00
Merge remote-tracking branch 'origin/main' into feature/restore-history
This commit is contained in:
@@ -1,26 +1,35 @@
|
||||
package signserver
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"erupe-ce/common/mhfcourse"
|
||||
"erupe-ce/common/token"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
func (s *Server) registerDBAccount(username string, password string) error {
|
||||
_, err := s.db.Exec("INSERT INTO users (username, password) VALUES ($1, $2)", username, password)
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s *Server) newUserChara(uid uint32) error {
|
||||
var numNewChars int
|
||||
err := s.db.QueryRow("SELECT COUNT(*) FROM characters WHERE user_id = $1 AND is_new_character = true", uid).Scan(&numNewChars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id int
|
||||
err = s.db.QueryRow("SELECT id FROM users WHERE username = $1", username).Scan(&id)
|
||||
if err != nil {
|
||||
// prevent users with an uninitialised character from creating more
|
||||
if numNewChars >= 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a base new character.
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO characters (
|
||||
user_id, is_female, is_new_character, small_gr_level, gr_override_mode, name, unk_desc_string,
|
||||
gr_override_level, gr_override_unk0, gr_override_unk1, exp, weapon, last_login)
|
||||
VALUES($1, False, True, 0, True, '', '', 0, 0, 0, 0, 0, $2)`,
|
||||
id,
|
||||
user_id, is_female, is_new_character, name, unk_desc_string,
|
||||
hrp, gr, weapon_type, last_login)
|
||||
VALUES($1, False, True, '', '', 0, 0, 0, $2)`,
|
||||
uid,
|
||||
uint32(time.Now().Unix()),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -30,27 +39,213 @@ func (s *Server) registerDBAccount(username string, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type character struct {
|
||||
ID uint32 `db:"id"`
|
||||
IsFemale bool `db:"is_female"`
|
||||
IsNewCharacter bool `db:"is_new_character"`
|
||||
SmallGRLevel uint8 `db:"small_gr_level"`
|
||||
GROverrideMode bool `db:"gr_override_mode"`
|
||||
Name string `db:"name"`
|
||||
UnkDescString string `db:"unk_desc_string"`
|
||||
GROverrideLevel uint16 `db:"gr_override_level"`
|
||||
GROverrideUnk0 uint8 `db:"gr_override_unk0"`
|
||||
GROverrideUnk1 uint8 `db:"gr_override_unk1"`
|
||||
Exp uint16 `db:"exp"`
|
||||
Weapon uint16 `db:"weapon"`
|
||||
LastLogin uint32 `db:"last_login"`
|
||||
func (s *Server) registerDBAccount(username string, password string) (uint32, error) {
|
||||
var uid uint32
|
||||
s.logger.Info("Creating user", zap.String("User", username))
|
||||
|
||||
// Create salted hash of user password
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = s.db.QueryRow("INSERT INTO users (username, password, return_expires) VALUES ($1, $2, $3) RETURNING id", username, string(passwordHash), time.Now().Add(time.Hour*24*30)).Scan(&uid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (s *Server) getCharactersForUser(uid int) ([]character, error) {
|
||||
characters := []character{}
|
||||
err := s.db.Select(&characters, "SELECT id, is_female, is_new_character, small_gr_level, gr_override_mode, name, unk_desc_string, gr_override_level, gr_override_unk0, gr_override_unk1, exp, weapon, last_login FROM characters WHERE user_id = $1", uid)
|
||||
type character struct {
|
||||
ID uint32 `db:"id"`
|
||||
IsFemale bool `db:"is_female"`
|
||||
IsNewCharacter bool `db:"is_new_character"`
|
||||
Name string `db:"name"`
|
||||
UnkDescString string `db:"unk_desc_string"`
|
||||
HRP uint16 `db:"hrp"`
|
||||
GR uint16 `db:"gr"`
|
||||
WeaponType uint16 `db:"weapon_type"`
|
||||
LastLogin uint32 `db:"last_login"`
|
||||
}
|
||||
|
||||
func (s *Server) getCharactersForUser(uid uint32) ([]character, error) {
|
||||
characters := make([]character, 0)
|
||||
err := s.db.Select(&characters, "SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = $1 AND deleted = false ORDER BY id", uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return characters, nil
|
||||
}
|
||||
|
||||
func (s *Server) getReturnExpiry(uid uint32) time.Time {
|
||||
var returnExpiry, lastLogin time.Time
|
||||
s.db.Get(&lastLogin, "SELECT COALESCE(last_login, now()) FROM users WHERE id=$1", uid)
|
||||
if time.Now().Add((time.Hour * 24) * -90).After(lastLogin) {
|
||||
returnExpiry = time.Now().Add(time.Hour * 24 * 30)
|
||||
s.db.Exec("UPDATE users SET return_expires=$1 WHERE id=$2", returnExpiry, uid)
|
||||
} else {
|
||||
err := s.db.Get(&returnExpiry, "SELECT return_expires FROM users WHERE id=$1", uid)
|
||||
if err != nil {
|
||||
returnExpiry = time.Now()
|
||||
s.db.Exec("UPDATE users SET return_expires=$1 WHERE id=$2", returnExpiry, uid)
|
||||
}
|
||||
}
|
||||
s.db.Exec("UPDATE users SET last_login=$1 WHERE id=$2", time.Now(), uid)
|
||||
return returnExpiry
|
||||
}
|
||||
|
||||
func (s *Server) getLastCID(uid uint32) uint32 {
|
||||
var lastPlayed uint32
|
||||
_ = s.db.QueryRow("SELECT last_character FROM users WHERE id=$1", uid).Scan(&lastPlayed)
|
||||
return lastPlayed
|
||||
}
|
||||
|
||||
func (s *Server) getUserRights(uid uint32) uint32 {
|
||||
var rights uint32
|
||||
if uid != 0 {
|
||||
_ = s.db.QueryRow("SELECT rights FROM users WHERE id=$1", uid).Scan(&rights)
|
||||
_, rights = mhfcourse.GetCourseStruct(rights)
|
||||
}
|
||||
return rights
|
||||
}
|
||||
|
||||
type members struct {
|
||||
CID uint32 // Local character ID
|
||||
ID uint32 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
func (s *Server) getFriendsForCharacters(chars []character) []members {
|
||||
friends := make([]members, 0)
|
||||
for _, char := range chars {
|
||||
friendsCSV := ""
|
||||
err := s.db.QueryRow("SELECT friends FROM characters WHERE id=$1", char.ID).Scan(&friendsCSV)
|
||||
friendsSlice := strings.Split(friendsCSV, ",")
|
||||
friendQuery := "SELECT id, name FROM characters WHERE id="
|
||||
for i := 0; i < len(friendsSlice); i++ {
|
||||
friendQuery += friendsSlice[i]
|
||||
if i+1 != len(friendsSlice) {
|
||||
friendQuery += " OR id="
|
||||
}
|
||||
}
|
||||
charFriends := make([]members, 0)
|
||||
err = s.db.Select(&charFriends, friendQuery)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for i := range charFriends {
|
||||
charFriends[i].CID = char.ID
|
||||
}
|
||||
friends = append(friends, charFriends...)
|
||||
}
|
||||
return friends
|
||||
}
|
||||
|
||||
func (s *Server) getGuildmatesForCharacters(chars []character) []members {
|
||||
guildmates := make([]members, 0)
|
||||
for _, char := range chars {
|
||||
var inGuild int
|
||||
_ = s.db.QueryRow("SELECT count(*) FROM guild_characters WHERE character_id=$1", char.ID).Scan(&inGuild)
|
||||
if inGuild > 0 {
|
||||
var guildID int
|
||||
err := s.db.QueryRow("SELECT guild_id FROM guild_characters WHERE character_id=$1", char.ID).Scan(&guildID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
charGuildmates := make([]members, 0)
|
||||
err = s.db.Select(&charGuildmates, "SELECT character_id AS id, c.name FROM guild_characters gc JOIN characters c ON c.id = gc.character_id WHERE guild_id=$1 AND character_id!=$2", guildID, char.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for i := range charGuildmates {
|
||||
charGuildmates[i].CID = char.ID
|
||||
}
|
||||
guildmates = append(guildmates, charGuildmates...)
|
||||
}
|
||||
}
|
||||
return guildmates
|
||||
}
|
||||
|
||||
func (s *Server) deleteCharacter(cid int, token string, tokenID uint32) error {
|
||||
if !s.validateToken(token, tokenID) {
|
||||
return errors.New("invalid token")
|
||||
}
|
||||
var isNew bool
|
||||
err := s.db.QueryRow("SELECT is_new_character FROM characters WHERE id = $1", cid).Scan(&isNew)
|
||||
if isNew {
|
||||
_, err = s.db.Exec("DELETE FROM characters WHERE id = $1", cid)
|
||||
} else {
|
||||
_, err = s.db.Exec("UPDATE characters SET deleted = true WHERE id = $1", cid)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unused
|
||||
func (s *Server) checkToken(uid uint32) (bool, error) {
|
||||
var exists int
|
||||
err := s.db.QueryRow("SELECT count(*) FROM sign_sessions WHERE user_id = $1", uid).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exists > 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *Server) registerUidToken(uid uint32) (uint32, string, error) {
|
||||
token := token.Generate(16)
|
||||
var tid uint32
|
||||
err := s.db.QueryRow(`INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id`, uid, token).Scan(&tid)
|
||||
return tid, token, err
|
||||
}
|
||||
|
||||
func (s *Server) registerPsnToken(psn string) (uint32, string, error) {
|
||||
token := token.Generate(16)
|
||||
var tid uint32
|
||||
err := s.db.QueryRow(`INSERT INTO sign_sessions (psn_id, token) VALUES ($1, $2) RETURNING id`, psn, token).Scan(&tid)
|
||||
return tid, token, err
|
||||
}
|
||||
|
||||
func (s *Server) validateToken(token string, tokenID uint32) bool {
|
||||
query := `SELECT count(*) FROM sign_sessions WHERE token = $1`
|
||||
if tokenID > 0 {
|
||||
query += ` AND id = $2`
|
||||
}
|
||||
var exists int
|
||||
err := s.db.QueryRow(query, token, tokenID).Scan(&exists)
|
||||
if err != nil || exists == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) validateLogin(user string, pass string) (uint32, RespID) {
|
||||
var uid uint32
|
||||
var passDB string
|
||||
err := s.db.QueryRow(`SELECT id, password FROM users WHERE username = $1`, user).Scan(&uid, &passDB)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
s.logger.Info("User not found", zap.String("User", user))
|
||||
if s.erupeConfig.DevMode && s.erupeConfig.DevModeOptions.AutoCreateAccount {
|
||||
uid, err = s.registerDBAccount(user, pass)
|
||||
if err == nil {
|
||||
return uid, SIGN_SUCCESS
|
||||
} else {
|
||||
return 0, SIGN_EABORT
|
||||
}
|
||||
}
|
||||
return 0, SIGN_EAUTH
|
||||
}
|
||||
return 0, SIGN_EABORT
|
||||
} else {
|
||||
if bcrypt.CompareHashAndPassword([]byte(passDB), []byte(pass)) == nil {
|
||||
return uid, SIGN_SUCCESS
|
||||
}
|
||||
return 0, SIGN_EPASS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,189 @@
|
||||
package signserver
|
||||
|
||||
import (
|
||||
"erupe-ce/common/byteframe"
|
||||
ps "erupe-ce/common/pascalstring"
|
||||
"erupe-ce/common/stringsupport"
|
||||
_config "erupe-ce/config"
|
||||
"erupe-ce/server/channelserver"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Andoryuuta/byteframe"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func paddedString(x string, size uint) []byte {
|
||||
out := make([]byte, size)
|
||||
copy(out, x)
|
||||
|
||||
// Null terminate it.
|
||||
out[len(out)-1] = 0
|
||||
return out
|
||||
}
|
||||
|
||||
func uint8PascalString(bf *byteframe.ByteFrame, x string) {
|
||||
bf.WriteUint8(uint8(len(x) + 1))
|
||||
bf.WriteNullTerminatedBytes([]byte(x))
|
||||
}
|
||||
|
||||
func uint16PascalString(bf *byteframe.ByteFrame, x string) {
|
||||
bf.WriteUint16(uint16(len(x) + 1))
|
||||
bf.WriteNullTerminatedBytes([]byte(x))
|
||||
}
|
||||
|
||||
func makeSignInFailureResp(respID RespID) []byte {
|
||||
bf := byteframe.NewByteFrame()
|
||||
bf.WriteUint8(uint8(respID))
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
func (s *Session) makeSignInResp(uid int) []byte {
|
||||
func (s *Session) makeSignResponse(uid uint32) []byte {
|
||||
// Get the characters from the DB.
|
||||
chars, err := s.server.getCharactersForUser(uid)
|
||||
if len(chars) == 0 && uid != 0 {
|
||||
err = s.server.newUserChara(uid)
|
||||
if err == nil {
|
||||
chars, err = s.server.getCharactersForUser(uid)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Warn("Error getting characters from DB", zap.Error(err))
|
||||
}
|
||||
|
||||
bf := byteframe.NewByteFrame()
|
||||
var tokenID uint32
|
||||
var sessToken string
|
||||
if uid == 0 && s.psn != "" {
|
||||
tokenID, sessToken, err = s.server.registerPsnToken(s.psn)
|
||||
} else {
|
||||
tokenID, sessToken, err = s.server.registerUidToken(uid)
|
||||
}
|
||||
if err != nil {
|
||||
bf.WriteUint8(uint8(SIGN_EABORT))
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
bf.WriteUint8(1) // resp_code
|
||||
bf.WriteUint8(0) // file/patch server count
|
||||
bf.WriteUint8(4) // entrance server count
|
||||
bf.WriteUint8(uint8(len(chars))) // character count
|
||||
bf.WriteUint32(0xFFFFFFFF) // login_token_number
|
||||
bf.WriteBytes(paddedString("logintokenstrng", 16)) // login_token (16 byte padded string)
|
||||
bf.WriteUint32(1576761190)
|
||||
uint8PascalString(bf, fmt.Sprintf("%s:%d", s.server.erupeConfig.HostIP, s.server.erupeConfig.Entrance.Port))
|
||||
uint8PascalString(bf, "")
|
||||
uint8PascalString(bf, "")
|
||||
uint8PascalString(bf, "mhf-n.capcom.com.tw")
|
||||
bf.WriteUint8(uint8(SIGN_SUCCESS)) // resp_code
|
||||
if (s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "") || s.client == PS3 {
|
||||
bf.WriteUint8(2)
|
||||
} else {
|
||||
bf.WriteUint8(0)
|
||||
}
|
||||
bf.WriteUint8(1) // entrance server count
|
||||
bf.WriteUint8(uint8(len(chars)))
|
||||
bf.WriteUint32(tokenID)
|
||||
bf.WriteBytes([]byte(sessToken))
|
||||
bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix()))
|
||||
if s.client == PS3 {
|
||||
ps.Uint8(bf, fmt.Sprintf(`ps3-%s.zerulight.cc`, s.server.erupeConfig.Language), false)
|
||||
ps.Uint8(bf, fmt.Sprintf(`ps3-%s.zerulight.cc`, s.server.erupeConfig.Language), false)
|
||||
} else {
|
||||
if s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" {
|
||||
ps.Uint8(bf, s.server.erupeConfig.PatchServerManifest, false)
|
||||
ps.Uint8(bf, s.server.erupeConfig.PatchServerFile, false)
|
||||
}
|
||||
}
|
||||
if strings.Split(s.rawConn.RemoteAddr().String(), ":")[0] == "127.0.0.1" {
|
||||
ps.Uint8(bf, fmt.Sprintf("127.0.0.1:%d", s.server.erupeConfig.Entrance.Port), false)
|
||||
} else {
|
||||
ps.Uint8(bf, fmt.Sprintf("%s:%d", s.server.erupeConfig.Host, s.server.erupeConfig.Entrance.Port), false)
|
||||
}
|
||||
|
||||
lastPlayed := uint32(0)
|
||||
for _, char := range chars {
|
||||
bf.WriteUint32(char.ID) // character ID 469153291
|
||||
if lastPlayed == 0 {
|
||||
lastPlayed = char.ID
|
||||
}
|
||||
bf.WriteUint32(char.ID)
|
||||
|
||||
// Exp, HR[x] is split by 0, 1, 30, 50, 99, 299, 998, 999
|
||||
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.MaxLauncherHR {
|
||||
bf.WriteUint16(999)
|
||||
} else {
|
||||
bf.WriteUint16(char.Exp)
|
||||
bf.WriteUint16(char.HRP)
|
||||
}
|
||||
|
||||
bf.WriteUint16(char.Weapon) // Weapon, 0-13.
|
||||
bf.WriteUint32(char.LastLogin) // Last login date, unix timestamp in seconds.
|
||||
bf.WriteBool(char.IsFemale) // Sex, 0=male, 1=female.
|
||||
bf.WriteBool(char.IsNewCharacter) // Is new character, 1 replaces character name with ?????.
|
||||
bf.WriteUint8(char.SmallGRLevel) // GR level if grMode == 0
|
||||
bf.WriteBool(char.GROverrideMode) // GR mode.
|
||||
bf.WriteBytes(paddedString(char.Name, 16)) // Character name
|
||||
bf.WriteBytes(paddedString(char.UnkDescString, 32)) // unk str
|
||||
if char.GROverrideMode {
|
||||
bf.WriteUint16(char.GROverrideLevel) // GR level override.
|
||||
bf.WriteUint8(char.GROverrideUnk0) // unk
|
||||
bf.WriteUint8(char.GROverrideUnk1) // unk
|
||||
bf.WriteUint16(char.WeaponType) // Weapon, 0-13.
|
||||
bf.WriteUint32(char.LastLogin) // Last login date, unix timestamp in seconds.
|
||||
bf.WriteBool(char.IsFemale) // Sex, 0=male, 1=female.
|
||||
bf.WriteBool(char.IsNewCharacter) // Is new character, 1 replaces character name with ?????.
|
||||
bf.WriteUint8(0) // Old GR
|
||||
bf.WriteBool(true) // Use uint16 GR, no reason not to
|
||||
bf.WriteBytes(stringsupport.PaddedString(char.Name, 16, true)) // Character name
|
||||
bf.WriteBytes(stringsupport.PaddedString(char.UnkDescString, 32, false)) // unk str
|
||||
if s.server.erupeConfig.RealClientMode >= _config.G7 {
|
||||
bf.WriteUint16(char.GR)
|
||||
bf.WriteUint8(0) // Unk
|
||||
bf.WriteUint8(0) // Unk
|
||||
}
|
||||
}
|
||||
|
||||
bf.WriteUint8(0) // friends_list_count
|
||||
bf.WriteUint8(0) // guild_members_count
|
||||
bf.WriteUint8(0) // notice_count
|
||||
bf.WriteUint32(0xDEADBEEF) // some_last_played_character_id
|
||||
bf.WriteUint32(14) // unk_flags
|
||||
uint8PascalString(bf, "") // unk_data_blob PascalString
|
||||
friends := s.server.getFriendsForCharacters(chars)
|
||||
if len(friends) == 0 {
|
||||
bf.WriteUint8(0)
|
||||
} else {
|
||||
if len(friends) > 255 {
|
||||
bf.WriteUint8(255)
|
||||
bf.WriteUint16(uint16(len(friends)))
|
||||
} else {
|
||||
bf.WriteUint8(uint8(len(friends)))
|
||||
}
|
||||
for _, friend := range friends {
|
||||
bf.WriteUint32(friend.CID)
|
||||
bf.WriteUint32(friend.ID)
|
||||
ps.Uint8(bf, friend.Name, true)
|
||||
}
|
||||
}
|
||||
|
||||
bf.WriteUint16(51728)
|
||||
bf.WriteUint16(20000)
|
||||
uint16PascalString(bf, "1000672925")
|
||||
guildmates := s.server.getGuildmatesForCharacters(chars)
|
||||
if len(guildmates) == 0 {
|
||||
bf.WriteUint8(0)
|
||||
} else {
|
||||
if len(guildmates) > 255 {
|
||||
bf.WriteUint8(255)
|
||||
bf.WriteUint16(uint16(len(guildmates)))
|
||||
} else {
|
||||
bf.WriteUint8(uint8(len(guildmates)))
|
||||
}
|
||||
for _, guildmate := range guildmates {
|
||||
bf.WriteUint32(guildmate.CID)
|
||||
bf.WriteUint32(guildmate.ID)
|
||||
ps.Uint8(bf, guildmate.Name, true)
|
||||
}
|
||||
}
|
||||
|
||||
bf.WriteUint8(0)
|
||||
if s.server.erupeConfig.HideLoginNotice {
|
||||
bf.WriteBool(false)
|
||||
} else {
|
||||
bf.WriteBool(true)
|
||||
bf.WriteUint8(0)
|
||||
bf.WriteUint8(0)
|
||||
ps.Uint16(bf, strings.Join(s.server.erupeConfig.LoginNotices[:], "<PAGE>"), true)
|
||||
}
|
||||
|
||||
bf.WriteUint16(51729)
|
||||
bf.WriteUint16(1)
|
||||
bf.WriteUint16(20000)
|
||||
uint16PascalString(bf, "203.191.249.36:8080")
|
||||
|
||||
bf.WriteUint32(1578905116)
|
||||
bf.WriteUint32(s.server.getLastCID(uid))
|
||||
bf.WriteUint32(s.server.getUserRights(uid))
|
||||
ps.Uint16(bf, "", false) // filters
|
||||
if s.client == VITA || s.client == PS3 {
|
||||
var psnUser string
|
||||
s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser)
|
||||
bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true))
|
||||
}
|
||||
bf.WriteUint16(0xCA10)
|
||||
bf.WriteUint16(0x4E20)
|
||||
ps.Uint16(bf, "", false) // unk key
|
||||
bf.WriteUint8(0x00)
|
||||
bf.WriteUint16(0xCA11)
|
||||
bf.WriteUint16(0x0001)
|
||||
bf.WriteUint16(0x4E20)
|
||||
ps.Uint16(bf, "", false) // unk ipv4
|
||||
bf.WriteUint32(uint32(s.server.getReturnExpiry(uid).Unix()))
|
||||
bf.WriteUint32(0)
|
||||
|
||||
mezfes := s.server.erupeConfig.DevModeOptions.MezFesEvent
|
||||
alt := s.server.erupeConfig.DevModeOptions.MezFesAlt
|
||||
if mezfes {
|
||||
// We can just use the start timestamp as the event ID
|
||||
bf.WriteUint32(uint32(channelserver.TimeWeekStart().Unix()))
|
||||
// Start time
|
||||
bf.WriteUint32(uint32(channelserver.TimeWeekStart().Unix()))
|
||||
// End time
|
||||
bf.WriteUint32(uint32(channelserver.TimeWeekNext().Unix()))
|
||||
bf.WriteUint8(2) // Unk
|
||||
bf.WriteUint32(s.server.erupeConfig.GameplayOptions.MezfesSoloTickets)
|
||||
bf.WriteUint32(s.server.erupeConfig.GameplayOptions.MezfesGroupTickets)
|
||||
bf.WriteUint8(8) // Stalls open
|
||||
bf.WriteUint8(10) // Stall Map
|
||||
bf.WriteUint8(3) // Pachinko
|
||||
bf.WriteUint8(6) // Nyanrendo
|
||||
bf.WriteUint8(9) // Point stall
|
||||
if alt {
|
||||
bf.WriteUint8(2) // Tokotoko Partnya
|
||||
} else {
|
||||
bf.WriteUint8(4) // Volpakkun Together
|
||||
}
|
||||
bf.WriteUint8(8) // Dokkan Battle Cats
|
||||
bf.WriteUint8(5) // Goocoo Scoop
|
||||
bf.WriteUint8(7) // Honey Panic
|
||||
} else {
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint32(0)
|
||||
}
|
||||
return bf.Data()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package signserver
|
||||
|
||||
//revive:disable
|
||||
type RespID uint8
|
||||
|
||||
type RespID uint16
|
||||
|
||||
//go:generate stringer -type=RespID
|
||||
const (
|
||||
SIGN_UNKNOWN RespID = iota
|
||||
SIGN_SUCCESS
|
||||
|
||||
@@ -3,137 +3,202 @@ package signserver
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"erupe-ce/common/stringsupport"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Andoryuuta/Erupe/network"
|
||||
"github.com/Andoryuuta/byteframe"
|
||||
"erupe-ce/common/byteframe"
|
||||
"erupe-ce/network"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type client int
|
||||
|
||||
const (
|
||||
PC100 client = iota
|
||||
VITA
|
||||
PS3
|
||||
WIIU
|
||||
)
|
||||
|
||||
// Session holds state for the sign server connection.
|
||||
type Session struct {
|
||||
sync.Mutex
|
||||
logger *zap.Logger
|
||||
sid int
|
||||
server *Server
|
||||
rawConn *net.Conn
|
||||
rawConn net.Conn
|
||||
cryptConn *network.CryptConn
|
||||
}
|
||||
|
||||
func (s *Session) fail() {
|
||||
s.server.Lock()
|
||||
delete(s.server.sessions, s.sid)
|
||||
s.server.Unlock()
|
||||
|
||||
client client
|
||||
psn string
|
||||
}
|
||||
|
||||
func (s *Session) work() {
|
||||
for {
|
||||
pkt, err := s.cryptConn.ReadPacket()
|
||||
if err != nil {
|
||||
s.fail()
|
||||
return
|
||||
}
|
||||
pkt, err := s.cryptConn.ReadPacket()
|
||||
|
||||
err = s.handlePacket(pkt)
|
||||
if err != nil {
|
||||
s.fail()
|
||||
return
|
||||
}
|
||||
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages {
|
||||
fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.handlePacket(pkt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) handlePacket(pkt []byte) error {
|
||||
sugar := s.logger.Sugar()
|
||||
|
||||
bf := byteframe.NewByteFrameFromBytes(pkt)
|
||||
reqType := string(bf.ReadNullTerminatedBytes())
|
||||
switch reqType {
|
||||
case "DLTSKEYSIGN:100":
|
||||
fallthrough
|
||||
case "DSGN:100":
|
||||
err := s.handleDSGNRequest(bf)
|
||||
if err != nil {
|
||||
return nil
|
||||
switch reqType[:len(reqType)-3] {
|
||||
case "DLTSKEYSIGN:", "DSGN:":
|
||||
s.handleDSGN(bf)
|
||||
case "PS3SGN:":
|
||||
s.client = PS3
|
||||
s.handlePSSGN(bf)
|
||||
case "VITASGN:":
|
||||
s.client = VITA
|
||||
s.handlePSSGN(bf)
|
||||
case "WIIUSGN:":
|
||||
s.client = WIIU
|
||||
s.handleWIIUSGN(bf)
|
||||
case "VITACOGLNK:", "COGLNK:":
|
||||
s.handlePSNLink(bf)
|
||||
case "DELETE:":
|
||||
token := string(bf.ReadNullTerminatedBytes())
|
||||
characterID := int(bf.ReadUint32())
|
||||
tokenID := bf.ReadUint32()
|
||||
err := s.server.deleteCharacter(characterID, token, tokenID)
|
||||
if err == nil {
|
||||
s.logger.Info("Deleted character", zap.Int("CharacterID", characterID))
|
||||
s.cryptConn.SendPacket([]byte{0x01}) // DEL_SUCCESS
|
||||
}
|
||||
case "DELETE:100":
|
||||
loginTokenString := string(bf.ReadNullTerminatedBytes())
|
||||
_ = loginTokenString
|
||||
characterID := bf.ReadUint32()
|
||||
|
||||
sugar.Infof("Got delete request for character ID: %v\n", characterID)
|
||||
sugar.Infof("remaining unknown data:\n%s\n", hex.Dump(bf.DataFromCurrent()))
|
||||
default:
|
||||
sugar.Infof("Got unknown request type %s, data:\n%s\n", reqType, hex.Dump(bf.DataFromCurrent()))
|
||||
s.logger.Warn("Unknown request", zap.String("reqType", reqType))
|
||||
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages {
|
||||
fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) handleDSGNRequest(bf *byteframe.ByteFrame) error {
|
||||
|
||||
reqUsername := string(bf.ReadNullTerminatedBytes())
|
||||
reqPassword := string(bf.ReadNullTerminatedBytes())
|
||||
reqUnk := string(bf.ReadNullTerminatedBytes())
|
||||
|
||||
s.server.logger.Info(
|
||||
"Got sign in request",
|
||||
zap.String("reqUsername", reqUsername),
|
||||
zap.String("reqPassword", reqPassword),
|
||||
zap.String("reqUnk", reqUnk),
|
||||
)
|
||||
|
||||
// TODO(Andoryuuta): remove plaintext password storage if this ever becomes more than a toy project.
|
||||
var (
|
||||
id int
|
||||
password string
|
||||
)
|
||||
err := s.server.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqUsername).Scan(&id, &password)
|
||||
var serverRespBytes []byte
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
s.logger.Info("Account not found", zap.String("reqUsername", reqUsername))
|
||||
serverRespBytes = makeSignInFailureResp(SIGN_EAUTH)
|
||||
|
||||
// HACK(Andoryuuta): Create a new account if it doesn't exit.
|
||||
s.logger.Info("Creating account", zap.String("reqUsername", reqUsername), zap.String("reqPassword", reqPassword))
|
||||
err = s.server.registerDBAccount(reqUsername, reqPassword)
|
||||
if err != nil {
|
||||
s.logger.Info("Error on creating new account", zap.Error(err))
|
||||
serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
|
||||
break
|
||||
}
|
||||
|
||||
var id int
|
||||
err = s.server.db.QueryRow("SELECT id FROM users WHERE username = $1", reqUsername).Scan(&id)
|
||||
if err != nil {
|
||||
s.logger.Info("Error on querying account id", zap.Error(err))
|
||||
serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
|
||||
break
|
||||
}
|
||||
|
||||
serverRespBytes = s.makeSignInResp(id)
|
||||
break
|
||||
case err != nil:
|
||||
serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
|
||||
s.logger.Warn("Got error on SQL query", zap.Error(err))
|
||||
break
|
||||
default:
|
||||
if reqPassword == password {
|
||||
s.logger.Info("Passwords match!")
|
||||
serverRespBytes = s.makeSignInResp(id)
|
||||
} else {
|
||||
s.logger.Info("Passwords don't match!")
|
||||
serverRespBytes = makeSignInFailureResp(SIGN_EPASS)
|
||||
}
|
||||
|
||||
func (s *Session) authenticate(username string, password string) {
|
||||
newCharaReq := false
|
||||
if username[len(username)-1] == 43 { // '+'
|
||||
username = username[:len(username)-1]
|
||||
newCharaReq = true
|
||||
}
|
||||
bf := byteframe.NewByteFrame()
|
||||
uid, resp := s.server.validateLogin(username, password)
|
||||
switch resp {
|
||||
case SIGN_SUCCESS:
|
||||
if newCharaReq {
|
||||
_ = s.server.newUserChara(uid)
|
||||
}
|
||||
bf.WriteBytes(s.makeSignResponse(uid))
|
||||
default:
|
||||
bf.WriteUint8(uint8(resp))
|
||||
}
|
||||
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogOutboundMessages {
|
||||
fmt.Printf("\n[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(bf.Data()), hex.Dump(bf.Data()))
|
||||
}
|
||||
_ = s.cryptConn.SendPacket(bf.Data())
|
||||
}
|
||||
|
||||
err = s.cryptConn.SendPacket(serverRespBytes)
|
||||
func (s *Session) handleWIIUSGN(bf *byteframe.ByteFrame) {
|
||||
_ = bf.ReadBytes(1)
|
||||
wiiuKey := string(bf.ReadBytes(64))
|
||||
var uid uint32
|
||||
err := s.server.db.QueryRow(`SELECT id FROM users WHERE wiiu_key = $1`, wiiuKey).Scan(&uid)
|
||||
if err != nil {
|
||||
return err
|
||||
if err == sql.ErrNoRows {
|
||||
s.logger.Info("Unlinked Wii U attempted to authenticate", zap.String("Key", wiiuKey))
|
||||
s.sendCode(SIGN_ECOGLINK)
|
||||
return
|
||||
}
|
||||
s.sendCode(SIGN_EABORT)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
s.cryptConn.SendPacket(s.makeSignResponse(uid))
|
||||
}
|
||||
|
||||
func (s *Session) handlePSSGN(bf *byteframe.ByteFrame) {
|
||||
// Prevent reading malformed request
|
||||
if len(bf.DataFromCurrent()) < 128 {
|
||||
s.sendCode(SIGN_EABORT)
|
||||
return
|
||||
}
|
||||
_ = bf.ReadNullTerminatedBytes() // VITA = 0000000256, PS3 = 0000000255
|
||||
_ = bf.ReadBytes(2) // VITA = 1, PS3 = !
|
||||
_ = bf.ReadBytes(82)
|
||||
s.psn = string(bf.ReadNullTerminatedBytes())
|
||||
var uid uint32
|
||||
err := s.server.db.QueryRow(`SELECT id FROM users WHERE psn_id = $1`, s.psn).Scan(&uid)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
s.cryptConn.SendPacket(s.makeSignResponse(0))
|
||||
return
|
||||
}
|
||||
s.sendCode(SIGN_EABORT)
|
||||
return
|
||||
}
|
||||
s.cryptConn.SendPacket(s.makeSignResponse(uid))
|
||||
}
|
||||
|
||||
func (s *Session) handlePSNLink(bf *byteframe.ByteFrame) {
|
||||
_ = bf.ReadNullTerminatedBytes() // Client ID
|
||||
credentials := strings.Split(stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()), "\n")
|
||||
token := string(bf.ReadNullTerminatedBytes())
|
||||
uid, resp := s.server.validateLogin(credentials[0], credentials[1])
|
||||
if resp == SIGN_SUCCESS && uid > 0 {
|
||||
var psn string
|
||||
err := s.server.db.QueryRow(`SELECT psn_id FROM sign_sessions WHERE token = $1`, token).Scan(&psn)
|
||||
if err != nil {
|
||||
s.sendCode(SIGN_ECOGLINK)
|
||||
return
|
||||
}
|
||||
|
||||
// Since we check for the psn_id, this will never run
|
||||
var exists int
|
||||
err = s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, psn).Scan(&exists)
|
||||
if err != nil {
|
||||
s.sendCode(SIGN_ECOGLINK)
|
||||
return
|
||||
} else if exists > 0 {
|
||||
s.sendCode(SIGN_EPSI)
|
||||
return
|
||||
}
|
||||
|
||||
var currentPSN string
|
||||
err = s.server.db.QueryRow(`SELECT COALESCE(psn_id, '') FROM users WHERE username = $1`, credentials[0]).Scan(¤tPSN)
|
||||
if err != nil {
|
||||
s.sendCode(SIGN_ECOGLINK)
|
||||
return
|
||||
} else if currentPSN != "" {
|
||||
s.sendCode(SIGN_EMBID)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.server.db.Exec(`UPDATE users SET psn_id = $1 WHERE username = $2`, psn, credentials[0])
|
||||
if err == nil {
|
||||
s.sendCode(SIGN_SUCCESS)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.sendCode(SIGN_ECOGLINK)
|
||||
}
|
||||
|
||||
func (s *Session) handleDSGN(bf *byteframe.ByteFrame) {
|
||||
user := stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes())
|
||||
pass := stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes())
|
||||
_ = string(bf.ReadNullTerminatedBytes()) // Unk
|
||||
s.authenticate(user, pass)
|
||||
}
|
||||
|
||||
func (s *Session) sendCode(id RespID) {
|
||||
s.cryptConn.SendPacket([]byte{byte(id)})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Andoryuuta/Erupe/config"
|
||||
"github.com/Andoryuuta/Erupe/network"
|
||||
"erupe-ce/network"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -16,15 +16,14 @@ import (
|
||||
type Config struct {
|
||||
Logger *zap.Logger
|
||||
DB *sqlx.DB
|
||||
ErupeConfig *config.Config
|
||||
ErupeConfig *_config.Config
|
||||
}
|
||||
|
||||
// Server is a MHF sign server.
|
||||
type Server struct {
|
||||
sync.Mutex
|
||||
logger *zap.Logger
|
||||
erupeConfig *config.Config
|
||||
sid int
|
||||
erupeConfig *_config.Config
|
||||
sessions map[int]*Session
|
||||
db *sqlx.DB
|
||||
listener net.Listener
|
||||
@@ -36,8 +35,6 @@ func NewServer(config *Config) *Server {
|
||||
s := &Server{
|
||||
logger: config.Logger,
|
||||
erupeConfig: config.ErupeConfig,
|
||||
sid: 0,
|
||||
sessions: make(map[int]*Session),
|
||||
db: config.DB,
|
||||
}
|
||||
return s
|
||||
@@ -58,7 +55,7 @@ func (s *Server) Start() error {
|
||||
|
||||
// Shutdown exits the server gracefully.
|
||||
func (s *Server) Shutdown() {
|
||||
s.logger.Debug("Shutting down")
|
||||
s.logger.Debug("Shutting down...")
|
||||
|
||||
s.Lock()
|
||||
s.isShuttingDown = true
|
||||
@@ -84,20 +81,19 @@ func (s *Server) acceptClients() {
|
||||
}
|
||||
}
|
||||
|
||||
go s.handleConnection(s.sid, conn)
|
||||
s.sid++
|
||||
go s.handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(sid int, conn net.Conn) {
|
||||
s.logger.Info("Got connection to sign server", zap.String("remoteaddr", conn.RemoteAddr().String()))
|
||||
func (s *Server) handleConnection(conn net.Conn) {
|
||||
s.logger.Debug("New connection", zap.String("RemoteAddr", conn.RemoteAddr().String()))
|
||||
defer conn.Close()
|
||||
|
||||
// Client initalizes the connection with a one-time buffer of 8 NULL bytes.
|
||||
nullInit := make([]byte, 8)
|
||||
_, err := io.ReadFull(conn, nullInit)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
conn.Close()
|
||||
s.logger.Error("Error initializing connection", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,15 +101,10 @@ func (s *Server) handleConnection(sid int, conn net.Conn) {
|
||||
session := &Session{
|
||||
logger: s.logger,
|
||||
server: s,
|
||||
rawConn: &conn,
|
||||
rawConn: conn,
|
||||
cryptConn: network.NewCryptConn(conn),
|
||||
}
|
||||
|
||||
// Add the session to the server's sessions map.
|
||||
s.Lock()
|
||||
s.sessions[sid] = session
|
||||
s.Unlock()
|
||||
|
||||
// Do the session's work.
|
||||
session.work()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user