diff --git a/patch-schema/psn-link.sql b/patch-schema/psn-link.sql new file mode 100644 index 000000000..fc22e9046 --- /dev/null +++ b/patch-schema/psn-link.sql @@ -0,0 +1,11 @@ +BEGIN; + +ALTER TABLE public.sign_sessions ADD COLUMN id SERIAL; + +ALTER TABLE public.sign_sessions ADD CONSTRAINT sign_sessions_pkey PRIMARY KEY (id); + +ALTER TABLE public.sign_sessions ALTER COLUMN user_id DROP NOT NULL; + +ALTER TABLE public.sign_sessions ADD COLUMN psn_id TEXT; + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 3e950657f..d93cf1a04 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -89,9 +89,15 @@ func parseChatCommand(s *Session, command string) { if err != nil || n != 1 { sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNError"], commands["PSN"].Prefix)) } else { - _, err = s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, id, s.charID) - if err == nil { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNSuccess"], id)) + var exists int + s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, id).Scan(&exists) + if exists == 0 { + _, err = s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, id, s.charID) + if err == nil { + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNSuccess"], id)) + } + } else { + sendServerChatMessage(s, s.server.dict["commandPSNExists"]) } } } diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index f393c5689..418e1cecb 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -22,6 +22,7 @@ func getLangStrings(s *Server) map[string]string { strings["commandTeleportSuccess"] = "%d %dにテレポート" strings["commandPSNError"] = "PSN連携コマンドエラー 例:%s " strings["commandPSNSuccess"] = "PSN「%s」が連携されています" + strings["commandPSNExists"] = "PSNは既存のユーザに接続されています" strings["commandRaviNoCommand"] = "ラヴィコマンドが指定されていません" strings["commandRaviStartSuccess"] = "大討伐を開始します" @@ -72,6 +73,7 @@ func getLangStrings(s *Server) map[string]string { strings["commandTeleportSuccess"] = "Teleporting to %d %d" strings["commandPSNError"] = "Error in command. Format: %s " strings["commandPSNSuccess"] = "Connected PSN ID: %s" + strings["commandPSNExists"] = "PSN ID is connected to another account!" strings["commandRaviNoCommand"] = "No Raviente command specified!" strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index 2720e07cd..751862a49 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -1,14 +1,18 @@ package signserver import ( + "database/sql" + "errors" "erupe-ce/common/mhfcourse" + "erupe-ce/common/token" "strings" "time" + "go.uber.org/zap" "golang.org/x/crypto/bcrypt" ) -func (s *Server) newUserChara(uid int) error { +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 { @@ -35,20 +39,22 @@ func (s *Server) newUserChara(uid int) error { return nil } -func (s *Server) registerDBAccount(username string, password string) (int, error) { +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 } - var id int - 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(&id) + 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 id, nil + return uid, nil } type character struct { @@ -63,7 +69,7 @@ type character struct { LastLogin uint32 `db:"last_login"` } -func (s *Server) getCharactersForUser(uid int) ([]character, error) { +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 { @@ -72,7 +78,7 @@ func (s *Server) getCharactersForUser(uid int) ([]character, error) { return characters, nil } -func (s *Server) getReturnExpiry(uid int) time.Time { +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) { @@ -89,16 +95,18 @@ func (s *Server) getReturnExpiry(uid int) time.Time { return returnExpiry } -func (s *Server) getLastCID(uid int) uint32 { +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 int) uint32 { - rights := uint32(2) - _ = s.db.QueryRow("SELECT rights FROM users WHERE id=$1", uid).Scan(&rights) - _, rights = mhfcourse.GetCourseStruct(rights) +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 } @@ -159,14 +167,12 @@ func (s *Server) getGuildmatesForCharacters(chars []character) []members { return guildmates } -func (s *Server) deleteCharacter(cid int, token string) error { - var verify int - err := s.db.QueryRow("SELECT count(*) FROM sign_sessions WHERE token = $1", token).Scan(&verify) - if err != nil { - return err // Invalid token +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) + 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 { @@ -179,7 +185,7 @@ func (s *Server) deleteCharacter(cid int, token string) error { } // Unused -func (s *Server) checkToken(uid int) (bool, error) { +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 { @@ -191,10 +197,55 @@ func (s *Server) checkToken(uid int) (bool, error) { return false, nil } -func (s *Server) registerToken(uid int, token string) error { - _, err := s.db.Exec("INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2)", uid, token) - if err != nil { - return err - } - return 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 + } } diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 948a5a011..77ac6468d 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -4,7 +4,6 @@ import ( "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" "erupe-ce/common/stringsupport" - "erupe-ce/common/token" _config "erupe-ce/config" "erupe-ce/server/channelserver" "fmt" @@ -12,10 +11,10 @@ import ( "strings" ) -func (s *Session) makeSignResponse(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 { + if len(chars) == 0 && uid != 0 { err = s.server.newUserChara(uid) if err == nil { chars, err = s.server.getCharactersForUser(uid) @@ -25,10 +24,18 @@ func (s *Session) makeSignResponse(uid int) []byte { s.logger.Warn("Error getting characters from DB", zap.Error(err)) } - sessToken := token.Generate(16) - _ = s.server.registerToken(uid, sessToken) - 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(uint8(SIGN_SUCCESS)) // resp_code if (s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "") || s.client == PS3 { @@ -38,7 +45,7 @@ func (s *Session) makeSignResponse(uid int) []byte { } bf.WriteUint8(1) // entrance server count bf.WriteUint8(uint8(len(chars))) - bf.WriteUint32(0xFFFFFFFF) // login_token_number + bf.WriteUint32(tokenID) bf.WriteBytes([]byte(sessToken)) bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix())) if s.client == PS3 { diff --git a/server/signserver/session.go b/server/signserver/session.go index 9207dda4b..5806f1108 100644 --- a/server/signserver/session.go +++ b/server/signserver/session.go @@ -3,20 +3,21 @@ package signserver import ( "database/sql" "encoding/hex" + "erupe-ce/common/stringsupport" "fmt" "net" + "strings" "sync" "erupe-ce/common/byteframe" "erupe-ce/network" "go.uber.org/zap" - "golang.org/x/crypto/bcrypt" ) -type Client int +type client int const ( - PC100 Client = iota + PC100 client = iota VITA PS3 WIIU @@ -29,7 +30,8 @@ type Session struct { server *Server rawConn net.Conn cryptConn *network.CryptConn - client Client + client client + psn string } func (s *Session) work() { @@ -51,23 +53,25 @@ func (s *Session) work() { func (s *Session) handlePacket(pkt []byte) error { bf := byteframe.NewByteFrameFromBytes(pkt) reqType := string(bf.ReadNullTerminatedBytes()) - switch reqType { - case "DLTSKEYSIGN:100", "DSGN:100": + switch reqType[:len(reqType)-3] { + case "DLTSKEYSIGN:", "DSGN:": s.handleDSGN(bf) - case "PS3SGN:100": + case "PS3SGN:": s.client = PS3 s.handlePSSGN(bf) - case "VITASGN:100", "VITASGN:000": + case "VITASGN:": s.client = VITA s.handlePSSGN(bf) - case "WIIUSGN:100", "WIIUSGN:000": + case "WIIUSGN:": s.client = WIIU s.handleWIIUSGN(bf) - case "DELETE:100": - loginTokenString := string(bf.ReadNullTerminatedBytes()) + case "VITACOGLNK:", "COGLNK:": + s.handlePSNLink(bf) + case "DELETE:": + token := string(bf.ReadNullTerminatedBytes()) characterID := int(bf.ReadUint32()) - _ = int(bf.ReadUint32()) // login_token_number - err := s.server.deleteCharacter(characterID, loginTokenString) + 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 @@ -83,104 +87,117 @@ func (s *Session) handlePacket(pkt []byte) error { func (s *Session) authenticate(username string, password string) { newCharaReq := false - if username[len(username)-1] == 43 { // '+' username = username[:len(username)-1] newCharaReq = true } - - var id int - var hash string bf := byteframe.NewByteFrame() - - err := s.server.db.QueryRow("SELECT id, password FROM users WHERE username = $1", username).Scan(&id, &hash) - switch { - case err == sql.ErrNoRows: - s.logger.Info("User not found", zap.String("Username", username)) - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.AutoCreateAccount { - s.logger.Info("Creating user", zap.String("Username", username)) - id, err = s.server.registerDBAccount(username, password) - if err == nil { - bf.WriteBytes(s.makeSignResponse(id)) - } - } else { - bf.WriteUint8(uint8(SIGN_EAUTH)) + uid, resp := s.server.validateLogin(username, password) + switch resp { + case SIGN_SUCCESS: + if newCharaReq { + _ = s.server.newUserChara(uid) } - case err != nil: - bf.WriteUint8(uint8(SIGN_EABORT)) - s.logger.Error("Error getting user details", zap.Error(err)) + bf.WriteBytes(s.makeSignResponse(uid)) default: - if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil || s.client == VITA || s.client == PS3 || s.client == WIIU { - s.logger.Debug("Passwords match!") - if newCharaReq { - err = s.server.newUserChara(id) - if err != nil { - s.logger.Error("Error adding new character to user", zap.Error(err)) - bf.WriteUint8(uint8(SIGN_EABORT)) - break - } - } - // TODO: Need to auto delete user tokens after inactivity - // exists, err := s.server.checkToken(id) - // if err != nil { - // s.logger.Info("Error checking for live tokens", zap.Error(err)) - // serverRespBytes = makeSignInFailureResp(SIGN_EABORT) - // break - // } - bf.WriteBytes(s.makeSignResponse(id)) - } else { - s.logger.Warn("Incorrect password") - bf.WriteUint8(uint8(SIGN_EPASS)) - } + 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())) } - - err = s.cryptConn.SendPacket(bf.Data()) + _ = s.cryptConn.SendPacket(bf.Data()) } func (s *Session) handleWIIUSGN(bf *byteframe.ByteFrame) { _ = bf.ReadBytes(1) wiiuKey := string(bf.ReadBytes(64)) - var reqUsername string - err := s.server.db.QueryRow(`SELECT username FROM users WHERE wiiu_key = $1`, wiiuKey).Scan(&reqUsername) - if err == sql.ErrNoRows { - resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(SIGN_ECOGLINK)) - s.cryptConn.SendPacket(resp.Data()) + var uid uint32 + err := s.server.db.QueryRow(`SELECT id FROM users WHERE wiiu_key = $1`, wiiuKey).Scan(&uid) + if err != nil { + 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 } - s.authenticate(reqUsername, "") + s.cryptConn.SendPacket(s.makeSignResponse(uid)) } func (s *Session) handlePSSGN(bf *byteframe.ByteFrame) { // Prevent reading malformed request if len(bf.DataFromCurrent()) < 128 { - resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(SIGN_EABORT)) - s.cryptConn.SendPacket(resp.Data()) + s.sendCode(SIGN_EABORT) return } _ = bf.ReadNullTerminatedBytes() // VITA = 0000000256, PS3 = 0000000255 _ = bf.ReadBytes(2) // VITA = 1, PS3 = ! _ = bf.ReadBytes(82) - psnUser := string(bf.ReadNullTerminatedBytes()) - var reqUsername string - err := s.server.db.QueryRow(`SELECT username FROM users WHERE psn_id = $1`, psnUser).Scan(&reqUsername) - if err == sql.ErrNoRows { - resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(SIGN_ECOGLINK)) - s.cryptConn.SendPacket(resp.Data()) + 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.authenticate(reqUsername, "") + 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) { - reqUsername := string(bf.ReadNullTerminatedBytes()) - reqPassword := string(bf.ReadNullTerminatedBytes()) + user := stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()) + pass := stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()) _ = string(bf.ReadNullTerminatedBytes()) // Unk - s.authenticate(reqUsername, reqPassword) + s.authenticate(user, pass) +} + +func (s *Session) sendCode(id RespID) { + s.cryptConn.SendPacket([]byte{byte(id)}) }