Skip to content

Commit

Permalink
[FEAT] Support Custom User Agent for browser apps (#3191)
Browse files Browse the repository at this point in the history
* [FEAT] Support Custom User Agent for browser apps

* feat: add fullscreen toggle to browser apps

* fix: missing client hints

* i18n: updated keys

* fix: typo

---------

Co-authored-by: Flavio F Lima <[email protected]>
  • Loading branch information
flavioislima and flavioislima authored Nov 4, 2023
1 parent f79f9e0 commit 97585d8
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 26 deletions.
7 changes: 5 additions & 2 deletions public/locales/en/gamepage.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,16 @@
"info": {
"broser": "BrowserURL",
"exe": "Select Executable",
"fullscreen": "Launch Fullscreen (F11 to exit)",
"image": "App Image",
"title": "Game/App Title"
"title": "Game/App Title",
"useragent": "Custom User Agent"
},
"placeholder": {
"image": "Paste an Image URL here",
"title": "Add a title to your Game/App",
"url": "Paste the Game URL here"
"url": "Paste the Game URL here",
"useragent": "Write a custom user agent here to be used on this browser app/game"
}
},
"specs": {
Expand Down
13 changes: 12 additions & 1 deletion src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
protocol,
screen,
clipboard,
components
components,
session
} from 'electron'
import 'backend/updater'
import { autoUpdater } from 'electron-updater'
Expand Down Expand Up @@ -298,6 +299,16 @@ if (!gotTheLock) {
initOnlineMonitor()
initImagesCache()

// Add User-Agent Client hints to behave like Windows
if (process.argv.includes('--spoof-windows')) {
session.defaultSession.webRequest.onBeforeSendHeaders(
(details, callback) => {
details.requestHeaders['sec-ch-ua-platform'] = 'Windows'
callback({ cancel: false, requestHeaders: details.requestHeaders })
}
)
}

if (!process.env.CI) {
await components.whenReady().catch((e) => {
logError([
Expand Down
18 changes: 18 additions & 0 deletions src/backend/preload.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { contextBridge } from 'electron'
import api from './api'

Expand All @@ -6,3 +8,19 @@ contextBridge.exposeInMainWorld(
'isSteamDeckGameMode',
process.env.XDG_CURRENT_DESKTOP === 'gamescope'
)

if (navigator.userAgent.includes('Windows')) {
Object.defineProperty(navigator, 'platform', {
get: function () {
return 'Win32'
},
set: function (a) {}
})

Object.defineProperty(navigator, 'userAgentData', {
get: function () {
return null
},
set: function (a) {}
})
}
8 changes: 6 additions & 2 deletions src/backend/storeManagers/sideload/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export function addNewApp({
art_square,
browserUrl,
is_installed = true,
description
description,
customUserAgent,
launchFullScreen
}: GameInfo): void {
const game: GameInfo = {
runner: 'sideload',
Expand All @@ -36,7 +38,9 @@ export function addNewApp({
art_square,
canRunOffline: !browserUrl,
browserUrl,
description
description,
customUserAgent,
launchFullScreen
}

if (isMac && executable?.endsWith('.app')) {
Expand Down
33 changes: 25 additions & 8 deletions src/backend/storeManagers/storeManagerCommon/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,25 @@ export function logFileLocation(appName: string) {
return join(gamesConfigPath, `${appName}-lastPlay.log`)
}

const openNewBrowserGameWindow = async (
browserUrl: string,
type BrowserGameOptions = {
browserUrl: string
abortId: string
): Promise<boolean> => {
customUserAgent?: string
launchFullScreen?: boolean
}

const openNewBrowserGameWindow = async ({
browserUrl,
abortId,
customUserAgent,
launchFullScreen
}: BrowserGameOptions): Promise<boolean> => {
const hostname = new URL(browserUrl).hostname

return new Promise((res) => {
const browserGame = new BrowserWindow({
icon: icon,
fullscreen: true,
fullscreen: launchFullScreen ?? false,
autoHideMenuBar: true,
webPreferences: {
partition: `persist:${hostname}`
Expand All @@ -60,9 +69,12 @@ const openNewBrowserGameWindow = async (
{ role: 'toggleDevTools' }
])
)

const defaultUserAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
browserGame.webContents.userAgent = customUserAgent ?? defaultUserAgent

browserGame.menuBarVisible = false
browserGame.webContents.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
browserGame.loadURL(browserUrl)
browserGame.on('ready-to-show', () => browserGame.show())

Expand Down Expand Up @@ -113,7 +125,7 @@ export async function launchGame(
install: { executable }
} = gameInfo

const { browserUrl } = gameInfo
const { browserUrl, customUserAgent, launchFullScreen } = gameInfo

const gameSettingsOverrides = await GameConfig.get(appName).getSettings()
if (
Expand All @@ -124,7 +136,12 @@ export async function launchGame(
}

if (browserUrl) {
return openNewBrowserGameWindow(browserUrl, appName)
return openNewBrowserGameWindow({
browserUrl,
abortId: appName,
customUserAgent,
launchFullScreen
})
}

const gameSettings = await getAppSettings(appName)
Expand Down
2 changes: 2 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export interface GameInfo {
//used for store release versions. if remote !== local, then update
version?: string
dlcList?: GameMetadataInner[]
customUserAgent?: string
launchFullScreen?: boolean
}

export interface GameSettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { InstallPlatform, WineInstallation, GameInfo } from 'common/types'
import {
CachedImage,
TextInputField,
PathSelectionBox
PathSelectionBox,
ToggleSwitch
} from 'frontend/components/UI'
import { DialogContent, DialogFooter } from 'frontend/components/UI/Dialog'
import {
Expand Down Expand Up @@ -52,6 +53,8 @@ export default function SideloadDialog({
)
const [selectedExe, setSelectedExe] = useState('')
const [gameUrl, setGameUrl] = useState('')
const [customUserAgent, setCustomUserAgent] = useState('')
const [launchFullScreen, setLaunchFullScreen] = useState(false)
const [imageUrl, setImageUrl] = useState('')
const [searching, setSearching] = useState(false)
const [app_name, setApp_name] = useState(appName ?? '')
Expand Down Expand Up @@ -81,7 +84,9 @@ export default function SideloadDialog({
art_square,
install: { executable, platform },
title,
browserUrl
browserUrl,
customUserAgent,
launchFullScreen
} = info

if (executable && platform) {
Expand All @@ -92,6 +97,15 @@ export default function SideloadDialog({
setGameUrl(browserUrl)
}

if (customUserAgent) {
setCustomUserAgent(customUserAgent)
}

console.log(launchFullScreen)
if (launchFullScreen !== undefined) {
setLaunchFullScreen(launchFullScreen)
}

setTitle(title)
setImageUrl(art_cover ? art_cover : art_square)
})
Expand Down Expand Up @@ -159,7 +173,9 @@ export default function SideloadDialog({
is_installed: true,
art_square: imageUrl ? imageUrl : fallbackImage,
canRunOffline: true,
browserUrl: gameUrl
browserUrl: gameUrl,
customUserAgent,
launchFullScreen
})
const gameSettings = await getGameSettings(app_name, 'sideload')
if (!gameSettings) {
Expand All @@ -176,6 +192,7 @@ export default function SideloadDialog({
})

await refreshLibrary({
library: 'sideload',
runInBackground: true,
checkForUpdates: true
})
Expand Down Expand Up @@ -322,16 +339,37 @@ export default function SideloadDialog({
/>
)}
{!showSideloadExe && (
<TextInputField
label={t('sideload.info.broser', 'BrowserURL')}
placeholder={t(
'sideload.placeholder.url',
'Paste the Game URL here'
)}
onChange={(e) => handleGameUrl(e.target.value)}
htmlId="sideload-game-url"
value={gameUrl}
/>
<>
<TextInputField
label={t('sideload.info.broser', 'BrowserURL')}
placeholder={t(
'sideload.placeholder.url',
'Paste the Game URL here'
)}
onChange={(e) => handleGameUrl(e.target.value)}
htmlId="sideload-game-url"
value={gameUrl}
/>
<TextInputField
label={t('sideload.info.useragent', 'Custom User Agent')}
placeholder={t(
'sideload.placeholder.useragent',
'Write a custom user agent here to be used on this browser app/game'
)}
onChange={(e) => setCustomUserAgent(e.target.value)}
htmlId="sideload-user-agent"
value={customUserAgent}
/>
<ToggleSwitch
htmlId="launch-fullscreen"
value={launchFullScreen}
handleChange={() => setLaunchFullScreen(!launchFullScreen)}
title={t(
'sideload.info.fullscreen',
'Launch Fullscreen (F11 to exit)'
)}
/>
</>
)}
</div>
</div>
Expand Down

0 comments on commit 97585d8

Please sign in to comment.