diff --git a/Package.resolved b/Package.resolved index 7971144..994e5f9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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", diff --git a/Package.swift b/Package.swift index 258a04f..e917f55 100644 --- a/Package.swift +++ b/Package.swift @@ -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 ), diff --git a/Sources/stella/Controllers/Api/ApiController.swift b/Sources/stella/Controllers/Api/ApiController.swift new file mode 100644 index 0000000..97c6276 --- /dev/null +++ b/Sources/stella/Controllers/Api/ApiController.swift @@ -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()) + } +} diff --git a/Sources/stella/Controllers/Api/ToolController.swift b/Sources/stella/Controllers/Api/ToolController.swift new file mode 100644 index 0000000..6bacfbc --- /dev/null +++ b/Sources/stella/Controllers/Api/ToolController.swift @@ -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 +} diff --git a/Sources/stella/Controllers/OpenApi/AuthController.swift b/Sources/stella/Controllers/OpenApi/AuthController.swift index b900e48..02423f5 100644 --- a/Sources/stella/Controllers/OpenApi/AuthController.swift +++ b/Sources/stella/Controllers/OpenApi/AuthController.swift @@ -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 } diff --git a/Sources/stella/Decoders/MsgPack.swift b/Sources/stella/Decoders/MsgPack.swift new file mode 100644 index 0000000..3ad01bc --- /dev/null +++ b/Sources/stella/Decoders/MsgPack.swift @@ -0,0 +1,18 @@ +import SwiftMsgpack +import Vapor + +extension MsgPackEncoder: @retroactive ContentEncoder, @retroactive @unchecked Sendable { + public func encode(_ 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(_ 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) + } +} diff --git a/Sources/stella/Migrations/CreatePlayers.swift b/Sources/stella/Migrations/CreatePlayers.swift new file mode 100644 index 0000000..42e0c21 --- /dev/null +++ b/Sources/stella/Migrations/CreatePlayers.swift @@ -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 { + + } +} diff --git a/Sources/stella/Models/Account.swift b/Sources/stella/Models/Account.swift index 1f2b752..2e0154e 100644 --- a/Sources/stella/Models/Account.swift +++ b/Sources/stella/Models/Account.swift @@ -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" diff --git a/Sources/stella/Models/Player.swift b/Sources/stella/Models/Player.swift new file mode 100644 index 0000000..846f3eb --- /dev/null +++ b/Sources/stella/Models/Player.swift @@ -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 + ) + + } + +} diff --git a/Sources/stella/routes.swift b/Sources/stella/routes.swift index 2f6b01c..54bf672 100644 --- a/Sources/stella/routes.swift +++ b/Sources/stella/routes.swift @@ -12,4 +12,5 @@ func routes(_ app: Application) throws { try app.register(collection: InfodeskController()) try app.register(collection: OpenApiController()) + try app.register(collection: ApiController()) } diff --git a/start-mitm.sh b/start-mitm.sh new file mode 100755 index 0000000..9cad39f --- /dev/null +++ b/start-mitm.sh @@ -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