Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 53 additions & 37 deletions opennow-stable/src/main/gfn/cloudmatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,21 @@ function buildSignalingUrl(
};
}

function requestHeaders(token: string): Record<string, string> {
const clientId = crypto.randomUUID();
const deviceId = crypto.randomUUID();
interface RequestHeadersOptions {
token: string;
clientId?: string;
deviceId?: string;
includeOrigin?: boolean;
}

return {
function requestHeaders(options: RequestHeadersOptions): Record<string, string> {
const clientId = options.clientId ?? crypto.randomUUID();
const deviceId = options.deviceId ?? crypto.randomUUID();

const headers: Record<string, string> = {
"User-Agent": GFN_USER_AGENT,
Authorization: `GFNJWT ${token}`,
Authorization: `GFNJWT ${options.token}`,
"Content-Type": "application/json",
Origin: "https://play.geforcenow.com",
Referer: "https://play.geforcenow.com/",
"nv-browser-type": "CHROME",
"nv-client-id": clientId,
"nv-client-streamer": "NVIDIA-CLASSIC",
Expand All @@ -388,6 +393,13 @@ function requestHeaders(token: string): Record<string, string> {
"nv-device-type": "DESKTOP",
"x-device-id": deviceId,
};

if (options.includeOrigin !== false) {
headers["Origin"] = "https://play.geforcenow.com";
headers["Referer"] = "https://play.geforcenow.com/";
}

return headers;
}

function parseResolution(input: string): { width: number; height: number } {
Expand Down Expand Up @@ -582,7 +594,16 @@ function extractSeatSetupStep(payload: CloudMatchResponse): number | undefined {
return undefined;
}

async function toSessionInfo(zone: string, streamingBaseUrl: string, payload: CloudMatchResponse): Promise<SessionInfo> {
interface ToSessionInfoOptions {
zone: string;
streamingBaseUrl: string;
payload: CloudMatchResponse;
clientId?: string;
deviceId?: string;
}

async function toSessionInfo(options: ToSessionInfoOptions): Promise<SessionInfo> {
const { zone, streamingBaseUrl, payload, clientId, deviceId } = options;
if (payload.requestStatus.statusCode !== 1) {
// Use SessionError for parsing error responses
const errorJson = JSON.stringify(payload);
Expand Down Expand Up @@ -624,6 +645,8 @@ async function toSessionInfo(zone: string, streamingBaseUrl: string, payload: Cl
gpuType: payload.session.gpuType,
iceServers: await normalizeIceServers(payload),
mediaConnectionInfo: signaling.mediaConnectionInfo,
clientId,
deviceId,
};
}

Expand All @@ -636,14 +659,18 @@ export async function createSession(input: SessionCreateRequest): Promise<Sessio
throw new Error(`Invalid launch appId '${input.appId}' (must be numeric)`);
}

// Generate client/device IDs once for the entire session lifecycle
const clientId = crypto.randomUUID();
const deviceId = crypto.randomUUID();

const body = buildSessionRequestBody(input);

const base = resolveStreamingBaseUrl(input.zone, input.streamingBaseUrl);
const languageCode = input.settings.gameLanguage ?? "en_US";
const url = `${base}/v2/session?keyboardLayout=en-US&languageCode=${languageCode}`;
const response = await fetch(url, {
method: "POST",
headers: requestHeaders(input.token),
headers: requestHeaders({ token: input.token, clientId, deviceId, includeOrigin: true }),
body: JSON.stringify(body),
});

Expand All @@ -654,17 +681,22 @@ export async function createSession(input: SessionCreateRequest): Promise<Sessio
}

const payload = JSON.parse(text) as CloudMatchResponse;
return await toSessionInfo(input.zone, base, payload);
return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId });
}

export async function pollSession(input: SessionPollRequest): Promise<SessionInfo> {
if (!input.token) {
throw new Error("Missing token for session polling");
}

// Use provided client/device IDs if available (should match session creation)
const clientId = input.clientId ?? crypto.randomUUID();
const deviceId = input.deviceId ?? crypto.randomUUID();

const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp);
const url = `${base}/v2/session/${input.sessionId}`;
const headers = requestHeaders(input.token);
// Polling should NOT include Origin/Referer headers (matches claimSession polling pattern)
const headers = requestHeaders({ token: input.token, clientId, deviceId, includeOrigin: false });
const response = await fetch(url, {
method: "GET",
headers,
Expand Down Expand Up @@ -707,7 +739,7 @@ export async function pollSession(input: SessionPollRequest): Promise<SessionInf
const directPayload = JSON.parse(directText) as CloudMatchResponse;
if (directPayload.requestStatus.statusCode === 1) {
console.log("[CloudMatch] Direct re-poll succeeded, using direct response for signaling info");
return await toSessionInfo(input.zone, directBase, directPayload);
return await toSessionInfo({ zone: input.zone, streamingBaseUrl: directBase, payload: directPayload, clientId, deviceId });
}
}
} catch (e) {
Expand All @@ -716,19 +748,23 @@ export async function pollSession(input: SessionPollRequest): Promise<SessionInf
}
}

return await toSessionInfo(input.zone, base, payload);
return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId });
}

export async function stopSession(input: SessionStopRequest): Promise<void> {
if (!input.token) {
throw new Error("Missing token for session stop");
}

// Use provided client/device IDs if available (should match session creation)
const clientId = input.clientId ?? crypto.randomUUID();
const deviceId = input.deviceId ?? crypto.randomUUID();

const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp);
const url = `${base}/v2/session/${input.sessionId}`;
const response = await fetch(url, {
method: "DELETE",
headers: requestHeaders(input.token),
headers: requestHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }),
});

if (!response.ok) {
Expand All @@ -750,30 +786,14 @@ export async function getActiveSessions(
throw new Error("Missing token for getting active sessions");
}

const deviceId = crypto.randomUUID();
const clientId = crypto.randomUUID();

const base = streamingBaseUrl.trim().endsWith("/")
? streamingBaseUrl.trim().slice(0, -1)
: streamingBaseUrl.trim();
const url = `${base}/v2/session`;

const headers: Record<string, string> = {
"User-Agent": GFN_USER_AGENT,
Authorization: `GFNJWT ${token}`,
"Content-Type": "application/json",
"nv-client-id": clientId,
"nv-client-streamer": "NVIDIA-CLASSIC",
"nv-client-type": "NATIVE",
"nv-client-version": GFN_CLIENT_VERSION,
"nv-device-os": process.platform === "win32" ? "WINDOWS" : process.platform === "darwin" ? "MACOS" : "LINUX",
"nv-device-type": "DESKTOP",
"x-device-id": deviceId,
};

const response = await fetch(url, {
method: "GET",
headers,
headers: requestHeaders({ token, includeOrigin: false }),
});

const text = await response.text();
Expand Down Expand Up @@ -946,9 +966,7 @@ export async function claimSession(input: SessionClaimRequest): Promise<SessionI
const zoneBase = `https://${effectiveServerIp}`;
const prefetchUrl = `${zoneBase}/v2/session/${input.sessionId}`;
console.log(`[CloudMatch] claimSession: pre-flight query ${prefetchUrl}`);
const prefetchHeaders = requestHeaders(input.token);
delete prefetchHeaders["Origin"];
delete prefetchHeaders["Referer"];
const prefetchHeaders = requestHeaders({ token: input.token, clientId, deviceId, includeOrigin: false });
try {
const prefetchResp = await fetch(prefetchUrl, { method: "GET", headers: prefetchHeaders });
console.log(`[CloudMatch] claimSession: pre-flight response status=${prefetchResp.status}`);
Expand All @@ -975,9 +993,7 @@ export async function claimSession(input: SessionClaimRequest): Promise<SessionI
// This prevents sending a claim to an expired/dead session
try {
const validationUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}`;
const validationHeaders = requestHeaders(input.token);
delete validationHeaders["Origin"];
delete validationHeaders["Referer"];
const validationHeaders = requestHeaders({ token: input.token, clientId, deviceId, includeOrigin: false });
const validationResp = await fetch(validationUrl, { method: "GET", headers: validationHeaders });
if (validationResp.ok) {
const validationText = await validationResp.text();
Expand Down
4 changes: 4 additions & 0 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,8 @@ export function App(): JSX.Element {
serverIp: newSession.serverIp,
zone: newSession.zone,
sessionId: newSession.sessionId,
clientId: newSession.clientId,
deviceId: newSession.deviceId,
});

setSession(polled);
Expand Down Expand Up @@ -1708,6 +1710,8 @@ export function App(): JSX.Element {
serverIp: current.serverIp,
zone: current.zone,
sessionId: current.sessionId,
clientId: current.clientId,
deviceId: current.deviceId,
});
}

Expand Down
6 changes: 6 additions & 0 deletions opennow-stable/src/shared/gfn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export interface SessionPollRequest {
serverIp?: string;
zone: string;
sessionId: string;
clientId?: string;
deviceId?: string;
}

export interface SessionStopRequest {
Expand All @@ -254,6 +256,8 @@ export interface SessionStopRequest {
serverIp?: string;
zone: string;
sessionId: string;
clientId?: string;
deviceId?: string;
}

export interface IceServer {
Expand All @@ -280,6 +284,8 @@ export interface SessionInfo {
gpuType?: string;
iceServers: IceServer[];
mediaConnectionInfo?: MediaConnectionInfo;
clientId?: string;
deviceId?: string;
}

/** Information about an active session from getActiveSessions */
Expand Down
Loading