diff --git a/components/databaseHandler.ts b/components/databaseHandler.ts index e3ef0b80c..5374327ad 100644 --- a/components/databaseHandler.ts +++ b/components/databaseHandler.ts @@ -415,6 +415,7 @@ export async function setupFileStructure(joinFunc = join) { "contracts", joinFunc("userdata", "epicids"), joinFunc("userdata", "steamids"), + joinFunc("userdata", "appleids"), joinFunc("userdata", "users"), joinFunc("userdata", "h1", "steamids"), joinFunc("userdata", "h1", "epicids"), diff --git a/components/generatedPeacockRequireTable.ts b/components/generatedPeacockRequireTable.ts index ccbba4a04..1a2ecc553 100644 --- a/components/generatedPeacockRequireTable.ts +++ b/components/generatedPeacockRequireTable.ts @@ -20,6 +20,7 @@ import * as commandService from "./commandService" import * as configSwizzleManager from "./configSwizzleManager" import * as controller from "./controller" import * as databaseHandler from "./databaseHandler" +import * as delegation from "./delegation" import * as entitlementStrategies from "./entitlementStrategies" import * as eventHandler from "./eventHandler" import * as evergreen from "./evergreen" @@ -85,6 +86,7 @@ export default { "@peacockproject/core/configSwizzleManager": configSwizzleManager, "@peacockproject/core/controller": controller, "@peacockproject/core/databaseHandler": databaseHandler, + "@peacockproject/core/delegation": delegation, "@peacockproject/core/entitlementStrategies": entitlementStrategies, "@peacockproject/core/eventHandler": eventHandler, "@peacockproject/core/evergreen": evergreen, diff --git a/components/index.ts b/components/index.ts index d107a113b..5acf5e650 100644 --- a/components/index.ts +++ b/components/index.ts @@ -125,7 +125,16 @@ const app = express() const baseDir = __dirname app.use(function badPathRewritingMiddleware(req, _, next) { - req.url = req.url.replaceAll("//", "/") + // rewrite every `//` to `/` that occurs before the query string + const qIdx = req.url.indexOf("?") + + if (qIdx === -1) { + req.url = req.url.replaceAll("//", "/") + } else { + req.url = + req.url.slice(0, qIdx).replaceAll("//", "/") + req.url.slice(qIdx) + } + next() }) app.use(loggingMiddleware) @@ -211,31 +220,39 @@ app.get( "pc-prod_6" } - if (req.query.issuer === STEAM_NAMESPACE_2021) { - config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = - "steam-prod_8" + switch (req.query.issuer) { + case STEAM_NAMESPACE_2021: + config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = + "steam-prod_8" + break + case "https://appleid.apple.com": + config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = + "apple-prod_8" + break } - if (req.params.audience === "scpc-prod") { + switch (req.params.audience) { // sniper challenge is a different game/audience - config.Versions[0].Name = "scpc-prod" - config.Versions[0].GAME_VER = "7.3.0" - config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = - "scpc-prod" + case "scpc-prod": + config.Versions[0].Name = "scpc-prod" + config.Versions[0].GAME_VER = "7.3.0" + config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = + "scpc-prod" + break + case "macos-prod": + config.Versions[0].Name = "macos-prod" + break + case "macossteam-prod": + config.Versions[0].Name = "macossteam-prod" + break } config.Versions[0].ISSUER_ID = req.query.issuer || "*" - config.Versions[0].SERVER_VER.Metrics.MetricsServerHost = `${proto}://${serverhost}` - config.Versions[0].SERVER_VER.Authentication.AuthenticationHost = `${proto}://${serverhost}` - config.Versions[0].SERVER_VER.Configuration.Url = `${proto}://${serverhost}/files/onlineconfig.json` - config.Versions[0].SERVER_VER.Configuration.AgreementUrl = `${proto}://${serverhost}/files/privacypolicy/hm3/privacypolicy.json` - config.Versions[0].SERVER_VER.Resources.ResourcesServicePath = `${proto}://${serverhost}/files` - config.Versions[0].SERVER_VER.GlobalAuthentication.AuthenticationHost = `${proto}://${serverhost}` res.json(config) diff --git a/components/oauthToken.ts b/components/oauthToken.ts index c2234dc50..6b7ef671e 100644 --- a/components/oauthToken.ts +++ b/components/oauthToken.ts @@ -49,9 +49,17 @@ export const JWT_SECRET = PEACOCK_DEV : randomBytes(32).toString("hex") export type OAuthTokenBody = { - grant_type: "external_steam" | "external_epic" | "refresh_token" + grant_type: + | "external_steam" + | "external_epic" + | "external_apple" + | "refresh_token" steam_userid?: string epic_userid?: string + apple_userid?: string + apple_refreshtoken?: string + device_os?: string + device_id?: string access_token: string pId?: string locale: string @@ -85,78 +93,93 @@ export async function handleOAuthToken( notBefore: -60000, expiresIn: 6000, issuer: "auth.hitman.io", - audience: isScpc ? "scpc-prod" : "pc_prod_8", + audience: (() => { + if (isScpc) return "scpc-prod" + if (req.body.grant_type === "external_apple") return "macos-prod" + return "pc_prod_8" + })(), noTimestamp: true, } - let external_platform: "steam" | "epic", + let external_platform: "steam" | "epic" | "apple", external_userid: string, - external_users_folder: "steamids" | "epicids", + external_users_folder: "steamids" | "epicids" | "appleids", external_appid: string - if (req.body.grant_type === "external_steam") { - if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) { - return error400 // invalid steam user id - } - - external_platform = "steam" - external_userid = req.body.steam_userid || "" - external_users_folder = "steamids" - external_appid = req.body.steam_appid - } else if (req.body.grant_type === "external_epic") { - if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) { - return error400 // invalid epic user id - } + switch (req.body.grant_type) { + case "external_steam": + if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) { + return error400 // invalid steam user id + } - const epic_token = decode( - req.body.access_token.replace(/^eg1~/, ""), - ) as { - appid: string - app: string - } + external_platform = "steam" + external_userid = req.body.steam_userid || "" + external_users_folder = "steamids" + external_appid = req.body.steam_appid + break + case "external_epic": { + if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) { + return error400 // invalid epic user id + } - if (!epic_token || !(epic_token.appid || epic_token.app)) { - return error400 // invalid epic access token - } + const epic_token = decode( + req.body.access_token.replace(/^eg1~/, ""), + ) as { + appid: string + app: string + } - external_appid = epic_token.appid || epic_token.app - external_platform = "epic" - external_userid = req.body.epic_userid || "" - external_users_folder = "epicids" - } else if (req.body.grant_type === "refresh_token") { - // send back the token from the request (re-signed so the timestamps update) - extractToken(req) // init req.jwt - // remove signOptions from existing jwt - // @ts-expect-error Non-optional, we're reassigning. - delete req.jwt.nbf // notBefore - // @ts-expect-error Non-optional, we're reassigning. - delete req.jwt.exp // expiresIn - // @ts-expect-error Non-optional, we're reassigning. - delete req.jwt.iss // issuer - // @ts-expect-error Non-optional, we're reassigning. - delete req.jwt.aud // audience - - if (!isScpc) { - if (userAuths.has(req.jwt.unique_name)) { - userAuths - .get(req.jwt.unique_name)! - ._doRefresh() - .then(() => undefined) - .catch(() => { - log(LogLevel.WARN, "Failed authentication refresh.") - userAuths.get(req.jwt.unique_name)!.initialized = false - }) + if (!epic_token || !(epic_token.appid || epic_token.app)) { + return error400 // invalid epic access token } - } - return { - access_token: sign(req.jwt, JWT_SECRET, signOptions), - token_type: "bearer", - expires_in: 5000, - refresh_token: randomUUID(), + external_appid = epic_token.appid || epic_token.app + external_platform = "epic" + external_userid = req.body.epic_userid || "" + external_users_folder = "epicids" + break } - } else { - return error406 // unsupported auth method + case "external_apple": + external_platform = "apple" + external_userid = req.body.apple_userid || "" + external_users_folder = "appleids" + external_appid = "apple" + break + case "refresh_token": + // send back the token from the request (re-signed so the timestamps update) + extractToken(req) // init req.jwt + // remove signOptions from existing jwt + // @ts-expect-error Non-optional, we're reassigning. + delete req.jwt.nbf // notBefore + // @ts-expect-error Non-optional, we're reassigning. + delete req.jwt.exp // expiresIn + // @ts-expect-error Non-optional, we're reassigning. + delete req.jwt.iss // issuer + // @ts-expect-error Non-optional, we're reassigning. + delete req.jwt.aud // audience + + if (!isScpc) { + if (userAuths.has(req.jwt.unique_name)) { + userAuths + .get(req.jwt.unique_name)! + ._doRefresh() + .then(() => undefined) + .catch(() => { + log(LogLevel.WARN, "Failed authentication refresh.") + userAuths.get(req.jwt.unique_name)!.initialized = + false + }) + } + } + + return { + access_token: sign(req.jwt, JWT_SECRET, signOptions), + token_type: "bearer", + expires_in: 5000, + refresh_token: randomUUID(), + } + default: + return error406 // unsupported auth method } if (req.body.pId && !uuidRegex.test(req.body.pId)) { @@ -165,7 +188,8 @@ export async function handleOAuthToken( const isHitman3 = external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" || - external_appid === STEAM_NAMESPACE_2021 + external_appid === STEAM_NAMESPACE_2021 || + external_platform === "apple" let gameVersion: GameVersion = "h1" @@ -253,6 +277,8 @@ export async function handleOAuthToken( userData.SteamId = req.body.steam_userid! } else if (external_platform === "epic") { userData.EpicId = req.body.epic_userid! + } else if (external_platform === "apple") { + userData.AppleId = req.body.apple_userid! } if (Object.hasOwn(userData.Extensions, "inventory")) { @@ -292,6 +318,9 @@ export async function handleOAuthToken( gameVersion, STEAM_NAMESPACE_2021, ).get(req.body.pId!) + } else if (external_platform === "apple") { + // TODO + return [] } else { log(LogLevel.ERROR, "Unsupported platform.") return [] diff --git a/components/types/types.ts b/components/types/types.ts index 6ab27f4e3..5a6927fc1 100644 --- a/components/types/types.ts +++ b/components/types/types.ts @@ -482,7 +482,10 @@ export type UserProfile = { steam?: string gog?: string xbox?: string + /** @deprecated */ stadia?: string + apple?: string + nintendo?: string } Extensions: { /** @@ -574,6 +577,7 @@ export type UserProfile = { DevId: string | null SteamId: string | null EpicId: string | null + AppleId?: string | null NintendoId: string | null XboxLiveId: string | null PSNAccountId: string | null diff --git a/patcher-darwin/.idea/editor.xml b/patcher-darwin/.idea/editor.xml index 8d0e15e94..169510248 100644 --- a/patcher-darwin/.idea/editor.xml +++ b/patcher-darwin/.idea/editor.xml @@ -208,7 +208,7 @@