add Player model

This commit is contained in:
Andrew Glaze
2025-05-18 16:53:08 -04:00
parent a93308afb1
commit 62260ffc73
11 changed files with 390 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
{
"originHash" : "01eb28d6d5d4bfc205f7c8b2f8a3dc3a3d278b26c75f239cbd1cf26e4482e33d",
"originHash" : "48b60a6f8caccb2179aca6ee731ee84b313e8f73da96743e41514dc5bf29a68e",
"pins" : [
{
"identity" : "async-http-client",
@@ -217,6 +217,15 @@
"version" : "2.7.0"
}
},
{
"identity" : "swift-msgpack",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nnabeyang/swift-msgpack.git",
"state" : {
"revision" : "1c8dbd506c68888df3fd5df4ff56cef9cb39c388",
"version" : "0.7.0"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",

View File

@@ -17,6 +17,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
// JWTs
.package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"),
// MsgPack
.package(url: "https://github.com/nnabeyang/swift-msgpack.git", from: "0.7.0")
],
targets: [
.executableTarget(
@@ -28,6 +30,7 @@ let package = Package(
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "JWT", package: "jwt"),
.product(name: "SwiftMsgpack", package: "swift-msgpack"),
],
swiftSettings: swiftSettings
),

View File

@@ -0,0 +1,8 @@
import Vapor
struct ApiController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let group = routes.grouped("latest", "api", "index.php")
try group.register(collection: ToolController())
}
}

View File

@@ -0,0 +1,34 @@
import Vapor
import SwiftMsgpack
import JWT
struct ToolController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let group = routes.grouped("tool")
group.post("signup", use: self.signup)
}
@Sendable
func signup(req: Request) async throws -> Response {
let body = try req.content.decode(SignupReq.self, using: MsgPackDecoder())
let session = try await req.jwt.verify(body.access_token, as: SessionPayload.self)
guard session.type == SessionType.ZAT.rawValue else {
throw Abort(.forbidden, reason: "Invalid access token")
}
throw Abort(.notImplemented)
}
}
struct SignupReq: Content {
let app_secret: String
let access_token: String
let storage_directory_path: String
let app_admin: String
let kakao_pid: String
let device_id: Double
let idp_code: String
}

View File

@@ -6,6 +6,7 @@ struct AuthController: RouteCollection {
routes.post("v4", "device", "accessToken", "create", use: self.createAccessToken)
routes.post("v3", "agreement", "getForLogin", use: self.loginAgreement)
routes.post("v4", "auth", "loginDevice", use: self.loginDevice)
routes.post("v3", "zat", "login", use: self.zatLogin)
}
@Sendable
@@ -67,14 +68,19 @@ struct AuthController: RouteCollection {
} else if let existingAccount = try await Account.query(on: req.db).filter(\.$idpId == idpId).first() {
account = existingAccount
} else {
let account = Account(appId: body.appId, idpAlias: idpAlias, idpCode: "zd3", idpId: idpId, status: "normal")
try await account.create(on: req.db)
account = Account(appId: body.appId, idpAlias: idpAlias, idpCode: "zd3", idpId: idpId, status: "normal")
try await account!.create(on: req.db)
}
guard let account = account else {
throw Abort(.badRequest)
}
if account.idpAlias != idpAlias {
account.idpAlias = idpAlias
try await account.save(on: req.db)
}
let zatExpiry = Date.now.advanced(by: 43200)
let zrtExpiry = Date.now.advanced(by: 2592000)
@@ -91,13 +97,17 @@ struct AuthController: RouteCollection {
externalToken: "",
zat: zatToken,
zrt: zrtToken,
player: Player(
player: LoginPlayer(
idpId: account.idpId,
appId: account.appId,
playerId: String(try account.requireID()),
agreemenet: nil,
pushOption: PushOptionResponse(night: "n", player: "n"),
regTime: Int(account.regDate.timeIntervalSince1970),
idpAlias: idpAlias,
idpCode: nil,
lang: nil,
lastLoginTime: nil,
firstLoginTime: Int(account.firstLogin.timeIntervalSince1970),
status: account.status
)
@@ -105,6 +115,110 @@ struct AuthController: RouteCollection {
return res
}
@Sendable
func zatLogin(req: Request) async throws -> LoginDeviceRes {
let body = try req.content.decode(ZatLoginReq.self, as: .json)
if let session = try? await req.jwt.verify(body.zat, as: SessionPayload.self) {
guard session.type == SessionType.ZAT.rawValue && session.accountId.value == body.playerId else {
throw Abort(.badRequest, reason: "Invalid zat provided.")
}
}
guard
let accountId = Int(body.playerId),
let account = try await Account.query(on: req.db)
.filter(\.$id == accountId)
.first()
else {
throw Abort(.badRequest, reason: "Invalid playerId")
}
account.lastLogin = Date.now
try await account.save(on: req.db)
let zatExpiry = Date.now.advanced(by: 43200)
let session = generateToken(accountId: try account.requireID(), expires: zatExpiry, type: SessionType.ZAT)
let zatToken = try await req.jwt.sign(session)
return LoginDeviceRes(
zatExpiryTime: Int(zatExpiry.timeIntervalSince1970) * 1000,
zrtExpiryTime: nil,
firstLogin: false,
externalToken: "",
zat: zatToken,
zrt: nil,
player: LoginPlayer(
idpId: account.idpId,
appId: account.appId,
playerId: String(accountId),
agreemenet: AgreementResponse(E001: "y", E002: "y", E006: "y", N002: "n", N003: "n", timestamp: "1717623430484"),
pushOption: PushOptionResponse(night: "n", player: "n"),
regTime: Int(account.regDate.timeIntervalSince1970) * 1000,
idpAlias: account.idpAlias,
idpCode: account.idpCode,
lang: body.lang,
lastLoginTime: Int(account.lastLogin.timeIntervalSince1970) * 1000,
firstLoginTime: Int(account.firstLogin.timeIntervalSince1970) * 1000,
status: account.status
)
)
}
}
struct ZatLoginReq: Content {
let adid: String
let appId: String
let appSecret: String
let appVer: String
let clientTime: Int
let country: String
let deviceId: String
let deviceModel: String
let fields: [String]
let gsiToken: Bool?
let lang: String
let loginType: String
let market: String
let network: String
let os: String
let playerId: String
let resume: Bool
let retryNo: Int?
let sdkVer: String
let telecom: String
let timezoneOffset: Int
let usimCountry: String?
let whiteKey: String
let zat: String
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
adid = try container.decode(String.self, forKey: .adid)
appId = try container.decode(String.self, forKey: .appId)
appSecret = try container.decode(String.self, forKey: .appSecret)
appVer = try container.decode(String.self, forKey: .appVer)
clientTime = try container.decode(Int.self, forKey: .clientTime)
country = try container.decode(String.self, forKey: .country)
deviceId = try container.decode(String.self, forKey: .deviceId)
deviceModel = try container.decode(String.self, forKey: .deviceModel)
fields = try container.decode([String].self, forKey: .fields)
gsiToken = try container.decodeIfPresent(Bool.self, forKey: .gsiToken)
lang = try container.decode(String.self, forKey: .lang)
loginType = try container.decode(String.self, forKey: .loginType)
market = try container.decode(String.self, forKey: .market)
network = try container.decode(String.self, forKey: .network)
os = try container.decode(String.self, forKey: .os)
playerId = try container.decode(String.self, forKey: .playerId)
resume = try container.decode(Bool.self, forKey: .resume)
retryNo = try container.decodeIfPresent(Int.self, forKey: .retryNo)
sdkVer = try container.decode(String.self, forKey: .sdkVer)
telecom = try container.decode(String.self, forKey: .telecom)
timezoneOffset = try container.decode(Int.self, forKey: .timezoneOffset)
usimCountry = try container.decodeIfPresent(String.self, forKey: .usimCountry)
whiteKey = try container.decode(String.self, forKey: .whiteKey)
zat = try container.decode(String.self, forKey: .zat)
}
}
struct AccessTokenRes: Content {
@@ -152,21 +266,25 @@ struct LoginDeviceReq: Content {
struct LoginDeviceRes: Content {
let zatExpiryTime: Int
let zrtExpiryTime: Int
let zrtExpiryTime: Int?
let firstLogin: Bool
let externalToken: String
let zat: String
let zrt: String
let player: Player
let zrt: String?
let player: LoginPlayer
}
struct Player: Content {
struct LoginPlayer: Content {
let idpId: String
let appId: String
let playerId: String
let agreemenet: AgreementResponse?
let pushOption: PushOptionResponse
let regTime: Int
let idpAlias: String
let idpCode: String?
let lang: String?
let lastLoginTime: Int?
let firstLoginTime: Int
let status: String
}

View File

@@ -0,0 +1,18 @@
import SwiftMsgpack
import Vapor
extension MsgPackEncoder: @retroactive ContentEncoder, @retroactive @unchecked Sendable {
public func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws where E : Encodable {
let data = try self.encode(encodable)
body.writeString(data.base64EncodedString())
}
}
extension MsgPackDecoder: @retroactive ContentDecoder, @retroactive @unchecked Sendable {
public func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D where D : Decodable {
guard let base64String = body.peekString(length: body.capacity), let data = Data(base64Encoded: base64String) else {
throw Abort(.badRequest)
}
return try self.decode(decodable, from: data)
}
}

View File

@@ -0,0 +1,39 @@
import Fluent
struct CreatePlayers: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("players")
.field("id", .int, .identifier(auto: true))
.field("stamina", .int, .required)
.field("stamina_heal_time", .date, .required)
.field("boost_point", .int, .required)
.field("boss_boost_point", .int, .required)
.field("transition_state", .int, .required)
.field("role", .int, .required)
.field("name", .string, .required)
.field("last_login_time", .date, .required)
.field("comment", .string, .required)
.field("vmoney", .int, .required)
.field("free_vmoney", .int, .required)
.field("rank_point", .int, .required)
.field("star_crumb", .int, .required)
.field("bond_token", .int, .required)
.field("exp_pool", .int, .required)
.field("exp_pooled_time", .date, .required)
.field("leader_character_id", .int, .required)
.field("party_slot", .int, .required)
.field("degree_id", .int, .required)
.field("birth", .int, .required)
.field("free_mana", .int, .required)
.field("paid_mana", .int, .required)
.field("enable_auto_3x", .bool, .required)
.field("account_id", .int, .required)
.field("tutorial_step", .int, .required)
.field("tutorial_skip_flag", .int, .required)
.create()
}
func revert(on database: any Database) async throws {
}
}

View File

@@ -1,8 +1,5 @@
import Fluent
/// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property
/// It is recommended you write your model with sendability checking on and then suppress the warning
/// afterwards with `@unchecked Sendable`.
final class Account: Model, @unchecked Sendable {
static let schema = "accounts"

View File

@@ -0,0 +1,151 @@
import Fluent
final class Player: Model, @unchecked Sendable {
static let schema = "players"
@ID(custom: "id", generatedBy: .database)
var id: Int?
@Field(key: "stamina")
var stamina: Int
@Field(key: "stamina_heal_time")
var staminaHealTime: Date
@Field(key: "boost_point")
var boostPoint: Int
@Field(key: "boss_boost_point")
var bossBoostPoint: Int
@Field(key: "transition_state")
var transitionState: Int
@Field(key: "role")
var role: Int
@Field(key: "name")
var name: String
@Field(key: "last_login_time")
var lastLoginTime: Date
@Field(key: "comment")
var comment: String
@Field(key: "vmoney")
var vmoney: Int
@Field(key: "free_vmoney")
var freeVmoney: Int
@Field(key: "rank_point")
var rankPoint: Int
@Field(key: "star_crumb")
var starCrumb: Int
@Field(key: "bond_token")
var bondToken: Int
@Field(key: "exp_pool")
var expPool: Int
@Field(key: "exp_pooled_time")
var expPooledTime: Date
@Field(key: "leader_character_id")
var leaderCharacterId: Int
@Field(key: "party_slot")
var partySlot: Int
@Field(key: "degree_id")
var degreeId: Int
@Field(key: "birth")
var birth: Int
@Field(key: "free_mana")
var freeMana: Int
@Field(key: "paid_mana")
var paidMana: Int
@Field(key: "enable_auto_3x")
var enableAuto3x: Bool
@Parent(key: "account_id")
var account: Account
@Field(key: "tutorial_step")
var tutorialStep: Int?
@Field(key: "tutorial_skip_flag")
var tutorialSkipFlag: Int?
init() { }
init(
stamina: Int,
staminaHealTime: Date,
boostPoint: Int,
bossBoostPoint: Int,
transitionState: Int,
role: Int,
name: String,
lastLoginTime: Date,
comment: String,
vmoney: Int,
freeVmoney: Int,
rankPoint: Int,
starCrumb: Int,
bondToken: Int,
expPool: Int,
expPooledTime: Date,
leaderCharacterId: Int,
partySlot: Int,
degreeId: Int,
birth: Int,
freeMana: Int,
paidMana: Int,
enableAuto3x: Bool,
account: Account,
tutorialStep: Int?,
tutorialSkipFlag: Int?
) {
self.stamina = stamina
self.staminaHealTime = staminaHealTime
self.boostPoint = boostPoint
self.bossBoostPoint = bossBoostPoint
self.transitionState = transitionState
self.role = role
self.name = name
self.lastLoginTime = lastLoginTime
self.comment = comment
self.vmoney = vmoney
self.freeVmoney = freeVmoney
self.rankPoint = rankPoint
self.starCrumb = starCrumb
self.bondToken = bondToken
self.expPool = expPool
self.expPooledTime = expPooledTime
self.leaderCharacterId = leaderCharacterId
self.partySlot = partySlot
self.degreeId = degreeId
self.birth = birth
self.freeMana = freeMana
self.paidMana = paidMana
self.enableAuto3x = enableAuto3x
self.tutorialStep = tutorialStep
self.tutorialSkipFlag = tutorialSkipFlag
}
static func createDefault(account: Account) -> Player {
return Player(
stamina: 20,
staminaHealTime: Date.now,
boostPoint: 3,
bossBoostPoint: 3,
transitionState: 0,
role: 1,
name: "플레이어",
lastLoginTime: Date.now,
comment: "Nice to meet you.",
vmoney: 0,
freeVmoney: 150,
rankPoint: 10,
starCrumb: 0,
bondToken: 0,
expPool: 0,
expPooledTime: Date.now,
leaderCharacterId: 1,
partySlot: 1,
degreeId: 1,
birth: 19900101,
freeMana: 1000,
paidMana: 0,
enableAuto3x: false,
account: account,
tutorialStep: 0,
tutorialSkipFlag: nil
)
}
}

View File

@@ -12,4 +12,5 @@ func routes(_ app: Application) throws {
try app.register(collection: InfodeskController())
try app.register(collection: OpenApiController())
try app.register(collection: ApiController())
}

1
start-mitm.sh Executable file
View File

@@ -0,0 +1 @@
mitmweb --set connection_strategy=lazy --allow-hosts gc-openapi-zinny3.kakaogames.com --allow-hosts gc-infodesk-zinny3.kakaogames.com --allow-hosts na.wdfp.kakaogames.com --allow-hosts patch.wdfp.kakaogames.com -s ./mitm-redirect-traffic.py