Skip to content

Commit c94fcd7

Browse files
authored
macOS server support (Steam and part of Apple) (#691)
## Scope The steam version works, the apple version doesn't yet. ## Test Plan Log in successfully from macOS Steam version of the game with `--disable-dynamic-resources`. ## Checklist -------- #### General - [x] I've run Prettier to format any changed files - [x] I've verified that my changes work, and included a test plan -------- #### Testing - [x] I have added or considered adding unit/integration tests that cover any code changes
2 parents 6f052bb + e88cb3a commit c94fcd7

7 files changed

Lines changed: 172 additions & 87 deletions

File tree

components/databaseHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ export async function setupFileStructure(joinFunc = join) {
415415
"contracts",
416416
joinFunc("userdata", "epicids"),
417417
joinFunc("userdata", "steamids"),
418+
joinFunc("userdata", "appleids"),
418419
joinFunc("userdata", "users"),
419420
joinFunc("userdata", "h1", "steamids"),
420421
joinFunc("userdata", "h1", "epicids"),

components/generatedPeacockRequireTable.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as commandService from "./commandService"
2020
import * as configSwizzleManager from "./configSwizzleManager"
2121
import * as controller from "./controller"
2222
import * as databaseHandler from "./databaseHandler"
23+
import * as delegation from "./delegation"
2324
import * as entitlementStrategies from "./entitlementStrategies"
2425
import * as eventHandler from "./eventHandler"
2526
import * as evergreen from "./evergreen"
@@ -85,6 +86,7 @@ export default {
8586
"@peacockproject/core/configSwizzleManager": configSwizzleManager,
8687
"@peacockproject/core/controller": controller,
8788
"@peacockproject/core/databaseHandler": databaseHandler,
89+
"@peacockproject/core/delegation": delegation,
8890
"@peacockproject/core/entitlementStrategies": entitlementStrategies,
8991
"@peacockproject/core/eventHandler": eventHandler,
9092
"@peacockproject/core/evergreen": evergreen,

components/index.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,16 @@ const app = express()
125125
const baseDir = __dirname
126126

127127
app.use(function badPathRewritingMiddleware(req, _, next) {
128-
req.url = req.url.replaceAll("//", "/")
128+
// rewrite every `//` to `/` that occurs before the query string
129+
const qIdx = req.url.indexOf("?")
130+
131+
if (qIdx === -1) {
132+
req.url = req.url.replaceAll("//", "/")
133+
} else {
134+
req.url =
135+
req.url.slice(0, qIdx).replaceAll("//", "/") + req.url.slice(qIdx)
136+
}
137+
129138
next()
130139
})
131140
app.use(loggingMiddleware)
@@ -211,31 +220,39 @@ app.get(
211220
"pc-prod_6"
212221
}
213222

214-
if (req.query.issuer === STEAM_NAMESPACE_2021) {
215-
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
216-
"steam-prod_8"
223+
switch (req.query.issuer) {
224+
case STEAM_NAMESPACE_2021:
225+
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
226+
"steam-prod_8"
227+
break
228+
case "https://appleid.apple.com":
229+
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
230+
"apple-prod_8"
231+
break
217232
}
218233

219-
if (req.params.audience === "scpc-prod") {
234+
switch (req.params.audience) {
220235
// sniper challenge is a different game/audience
221-
config.Versions[0].Name = "scpc-prod"
222-
config.Versions[0].GAME_VER = "7.3.0"
223-
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
224-
"scpc-prod"
236+
case "scpc-prod":
237+
config.Versions[0].Name = "scpc-prod"
238+
config.Versions[0].GAME_VER = "7.3.0"
239+
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
240+
"scpc-prod"
241+
break
242+
case "macos-prod":
243+
config.Versions[0].Name = "macos-prod"
244+
break
245+
case "macossteam-prod":
246+
config.Versions[0].Name = "macossteam-prod"
247+
break
225248
}
226249

227250
config.Versions[0].ISSUER_ID = req.query.issuer || "*"
228-
229251
config.Versions[0].SERVER_VER.Metrics.MetricsServerHost = `${proto}://${serverhost}`
230-
231252
config.Versions[0].SERVER_VER.Authentication.AuthenticationHost = `${proto}://${serverhost}`
232-
233253
config.Versions[0].SERVER_VER.Configuration.Url = `${proto}://${serverhost}/files/onlineconfig.json`
234-
235254
config.Versions[0].SERVER_VER.Configuration.AgreementUrl = `${proto}://${serverhost}/files/privacypolicy/hm3/privacypolicy.json`
236-
237255
config.Versions[0].SERVER_VER.Resources.ResourcesServicePath = `${proto}://${serverhost}/files`
238-
239256
config.Versions[0].SERVER_VER.GlobalAuthentication.AuthenticationHost = `${proto}://${serverhost}`
240257

241258
res.json(config)

components/oauthToken.ts

Lines changed: 91 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,17 @@ export const JWT_SECRET = PEACOCK_DEV
4949
: randomBytes(32).toString("hex")
5050

5151
export type OAuthTokenBody = {
52-
grant_type: "external_steam" | "external_epic" | "refresh_token"
52+
grant_type:
53+
| "external_steam"
54+
| "external_epic"
55+
| "external_apple"
56+
| "refresh_token"
5357
steam_userid?: string
5458
epic_userid?: string
59+
apple_userid?: string
60+
apple_refreshtoken?: string
61+
device_os?: string
62+
device_id?: string
5563
access_token: string
5664
pId?: string
5765
locale: string
@@ -85,78 +93,93 @@ export async function handleOAuthToken(
8593
notBefore: -60000,
8694
expiresIn: 6000,
8795
issuer: "auth.hitman.io",
88-
audience: isScpc ? "scpc-prod" : "pc_prod_8",
96+
audience: (() => {
97+
if (isScpc) return "scpc-prod"
98+
if (req.body.grant_type === "external_apple") return "macos-prod"
99+
return "pc_prod_8"
100+
})(),
89101
noTimestamp: true,
90102
}
91103

92-
let external_platform: "steam" | "epic",
104+
let external_platform: "steam" | "epic" | "apple",
93105
external_userid: string,
94-
external_users_folder: "steamids" | "epicids",
106+
external_users_folder: "steamids" | "epicids" | "appleids",
95107
external_appid: string
96108

97-
if (req.body.grant_type === "external_steam") {
98-
if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) {
99-
return error400 // invalid steam user id
100-
}
101-
102-
external_platform = "steam"
103-
external_userid = req.body.steam_userid || ""
104-
external_users_folder = "steamids"
105-
external_appid = req.body.steam_appid
106-
} else if (req.body.grant_type === "external_epic") {
107-
if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) {
108-
return error400 // invalid epic user id
109-
}
109+
switch (req.body.grant_type) {
110+
case "external_steam":
111+
if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) {
112+
return error400 // invalid steam user id
113+
}
110114

111-
const epic_token = decode(
112-
req.body.access_token.replace(/^eg1~/, ""),
113-
) as {
114-
appid: string
115-
app: string
116-
}
115+
external_platform = "steam"
116+
external_userid = req.body.steam_userid || ""
117+
external_users_folder = "steamids"
118+
external_appid = req.body.steam_appid
119+
break
120+
case "external_epic": {
121+
if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) {
122+
return error400 // invalid epic user id
123+
}
117124

118-
if (!epic_token || !(epic_token.appid || epic_token.app)) {
119-
return error400 // invalid epic access token
120-
}
125+
const epic_token = decode(
126+
req.body.access_token.replace(/^eg1~/, ""),
127+
) as {
128+
appid: string
129+
app: string
130+
}
121131

122-
external_appid = epic_token.appid || epic_token.app
123-
external_platform = "epic"
124-
external_userid = req.body.epic_userid || ""
125-
external_users_folder = "epicids"
126-
} else if (req.body.grant_type === "refresh_token") {
127-
// send back the token from the request (re-signed so the timestamps update)
128-
extractToken(req) // init req.jwt
129-
// remove signOptions from existing jwt
130-
// @ts-expect-error Non-optional, we're reassigning.
131-
delete req.jwt.nbf // notBefore
132-
// @ts-expect-error Non-optional, we're reassigning.
133-
delete req.jwt.exp // expiresIn
134-
// @ts-expect-error Non-optional, we're reassigning.
135-
delete req.jwt.iss // issuer
136-
// @ts-expect-error Non-optional, we're reassigning.
137-
delete req.jwt.aud // audience
138-
139-
if (!isScpc) {
140-
if (userAuths.has(req.jwt.unique_name)) {
141-
userAuths
142-
.get(req.jwt.unique_name)!
143-
._doRefresh()
144-
.then(() => undefined)
145-
.catch(() => {
146-
log(LogLevel.WARN, "Failed authentication refresh.")
147-
userAuths.get(req.jwt.unique_name)!.initialized = false
148-
})
132+
if (!epic_token || !(epic_token.appid || epic_token.app)) {
133+
return error400 // invalid epic access token
149134
}
150-
}
151135

152-
return {
153-
access_token: sign(req.jwt, JWT_SECRET, signOptions),
154-
token_type: "bearer",
155-
expires_in: 5000,
156-
refresh_token: randomUUID(),
136+
external_appid = epic_token.appid || epic_token.app
137+
external_platform = "epic"
138+
external_userid = req.body.epic_userid || ""
139+
external_users_folder = "epicids"
140+
break
157141
}
158-
} else {
159-
return error406 // unsupported auth method
142+
case "external_apple":
143+
external_platform = "apple"
144+
external_userid = req.body.apple_userid || ""
145+
external_users_folder = "appleids"
146+
external_appid = "apple"
147+
break
148+
case "refresh_token":
149+
// send back the token from the request (re-signed so the timestamps update)
150+
extractToken(req) // init req.jwt
151+
// remove signOptions from existing jwt
152+
// @ts-expect-error Non-optional, we're reassigning.
153+
delete req.jwt.nbf // notBefore
154+
// @ts-expect-error Non-optional, we're reassigning.
155+
delete req.jwt.exp // expiresIn
156+
// @ts-expect-error Non-optional, we're reassigning.
157+
delete req.jwt.iss // issuer
158+
// @ts-expect-error Non-optional, we're reassigning.
159+
delete req.jwt.aud // audience
160+
161+
if (!isScpc) {
162+
if (userAuths.has(req.jwt.unique_name)) {
163+
userAuths
164+
.get(req.jwt.unique_name)!
165+
._doRefresh()
166+
.then(() => undefined)
167+
.catch(() => {
168+
log(LogLevel.WARN, "Failed authentication refresh.")
169+
userAuths.get(req.jwt.unique_name)!.initialized =
170+
false
171+
})
172+
}
173+
}
174+
175+
return {
176+
access_token: sign(req.jwt, JWT_SECRET, signOptions),
177+
token_type: "bearer",
178+
expires_in: 5000,
179+
refresh_token: randomUUID(),
180+
}
181+
default:
182+
return error406 // unsupported auth method
160183
}
161184

162185
if (req.body.pId && !uuidRegex.test(req.body.pId)) {
@@ -165,7 +188,8 @@ export async function handleOAuthToken(
165188

166189
const isHitman3 =
167190
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
168-
external_appid === STEAM_NAMESPACE_2021
191+
external_appid === STEAM_NAMESPACE_2021 ||
192+
external_platform === "apple"
169193

170194
let gameVersion: GameVersion = "h1"
171195

@@ -253,6 +277,8 @@ export async function handleOAuthToken(
253277
userData.SteamId = req.body.steam_userid!
254278
} else if (external_platform === "epic") {
255279
userData.EpicId = req.body.epic_userid!
280+
} else if (external_platform === "apple") {
281+
userData.AppleId = req.body.apple_userid!
256282
}
257283

258284
if (Object.hasOwn(userData.Extensions, "inventory")) {
@@ -292,6 +318,9 @@ export async function handleOAuthToken(
292318
gameVersion,
293319
STEAM_NAMESPACE_2021,
294320
).get(req.body.pId!)
321+
} else if (external_platform === "apple") {
322+
// TODO
323+
return []
295324
} else {
296325
log(LogLevel.ERROR, "Unsupported platform.")
297326
return []

components/types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,10 @@ export type UserProfile = {
482482
steam?: string
483483
gog?: string
484484
xbox?: string
485+
/** @deprecated */
485486
stadia?: string
487+
apple?: string
488+
nintendo?: string
486489
}
487490
Extensions: {
488491
/**
@@ -574,6 +577,7 @@ export type UserProfile = {
574577
DevId: string | null
575578
SteamId: string | null
576579
EpicId: string | null
580+
AppleId?: string | null
577581
NintendoId: string | null
578582
XboxLiveId: string | null
579583
PSNAccountId: string | null

patcher-darwin/.idea/editor.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)