diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 2f62aee..e8311d0 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -1162,6 +1162,20 @@ function Get-LenovoDrivers { [string]$ModelName ) + # Lenovo is special - they prevent access to the PSREF API without a cookie as of July 2025. + # This cookie must be retrieved via Javascript + # It appears that the cookie is hard-coded. We'll see how long this lasts. + # If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know. + # https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models). + + # $lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg" + + # Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes. + $lenovoCookie = Get-LenovoPSREFToken + + # Add the cookie to the headers + $Headers["Cookie"] = $lenovoCookie + $url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$ModelName" WriteLog "Querying Lenovo PSREF API for model: $ModelName" $OriginalVerbosePreference = $VerbosePreference @@ -1174,23 +1188,25 @@ function Get-LenovoDrivers { $products = @() foreach ($item in $jsonResponse.data) { - $productName = $item.ProductName - $machineTypes = $item.MachineType -split " / " + if (-not [string]::IsNullOrEmpty($item.MachineType) -and -not [string]::IsNullOrEmpty($item.ProductName)) { + $productName = $item.ProductName + $machineTypes = $item.MachineType -split " / " - foreach ($machineType in $machineTypes) { - if ($machineType -eq $ModelName) { - WriteLog "Model name entered is a matching machine type" - $products = @() + foreach ($machineType in $machineTypes) { + if ($machineType -eq $ModelName) { + WriteLog "Model name entered is a matching machine type" + $products = @() + $products += [pscustomobject]@{ + ProductName = $productName + MachineType = $machineType + } + WriteLog "Product Name: $productName Machine Type: $machineType" + return $products + } $products += [pscustomobject]@{ ProductName = $productName MachineType = $machineType } - WriteLog "Product Name: $productName Machine Type: $machineType" - return $products - } - $products += [pscustomobject]@{ - ProductName = $productName - MachineType = $machineType } } } diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.psm1 index f0accca..e99968e 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.psm1 @@ -248,9 +248,145 @@ function Test-ExistingDriver { # If neither WIM nor a valid folder exists, return null return $null } +function Get-LenovoPSREFToken { + + <# + .DESCRIPTION + Retrieves the Lenovo PSREF token from the Edge browser's local storage. + + .NOTES + + Lenovo's PSREF site creates a cookie/token via javascript when navigating to the PSREF site. This cookie only needs + to be retrieved once on a single machine, and every machine within the same network will be able to access the PSREF API. + + Using Invoke-Webrequest with sessionvariable or websession doesn't work because the token is created by javascript. + Using edge in headless mode with remote debugging enabled allows for the retrieval of the token via the DevTools protocol. + + You couldn't be more unhappy about this solution than I am, but it works. + + Why use PSREF and not catalogv2.xml? Catalogv2.xml doesn't include all models. PSREF provides an API that can be used to retrieve + the friendly model and machine type information for both business and consumer models. Many EDU devices are deemed consumer. + + System Update and other tools rely on the user to input machine type and model information, but finding the machine type is difficult for some. + Our solution makes it easier to simply type the model name and you can match the machine type to the model name. + + If you have a better solution, please submit a PR or open a discussion on Github. Happy to consider alternatives. An easy way to test + if your alternative works is to see if you can retrieve 100e, 300w, 500w, etc. These don't show up in catalogv2.xml, but they do in PSREF. + #> + + # Path to Edge + $edgeExe = "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe" + + # Any free port works. 9222 is common. + $port = 9222 + $uri = 'https://psref.lenovo.com' + + # Headless run with remote debugging. + $flags = "--headless=new --disable-gpu --remote-debugging-port=$port $uri" + $edge = Start-Process -FilePath $edgeExe -ArgumentList $flags -PassThru + Writelog "Edge process started with PID: $($edge.Id)." + + # Wait a short moment so the target appears. + Start-Sleep -Seconds 3 + + # Find the first page target. + $targets = Invoke-RestMethod "http://localhost:$port/json" + $wsUrl = ($targets | Where-Object type -eq 'page')[0].webSocketDebuggerUrl + + # Connect to that WebSocket. + $socket = [System.Net.WebSockets.ClientWebSocket]::new() + $socket.ConnectAsync($wsUrl, [Threading.CancellationToken]::None).Wait() + + # Helper to send a DevTools command. + function Send-DevToolsCommand { + param([int]$id, [string]$method, [hashtable]$params = @{}) + $cmd = @{ id = $id; method = $method; params = $params } | + ConvertTo-Json -Compress + $data = [Text.Encoding]::UTF8.GetBytes($cmd) + $socket.SendAsync([ArraySegment[byte]]$data, 'Text', $true, + [Threading.CancellationToken]::None).Wait() + } + + # Ask the page to return localStorage['asut']. + Send-DevToolsCommand -id 1 -method 'Runtime.evaluate' -params @{ + expression = "localStorage.getItem('asut')" + } + + # Receive frames until the whole message arrives. + $ms = New-Object System.IO.MemoryStream + $buf = New-Object byte[] 8192 + do { + $seg = [ArraySegment[byte]]::new($buf) + $res = $socket.ReceiveAsync($seg, + [Threading.CancellationToken]::None).Result + $ms.Write($buf, 0, $res.Count) + } until ($res.EndOfMessage) + + $ms.Position = 0 + $json = ([System.IO.StreamReader]::new($ms, [Text.Encoding]::UTF8)).ReadToEnd() | + ConvertFrom-Json + + $token = $json.result.result.value + # Concatenate the token value with X-PSREF-USER-TOKEN= + $token = "X-PSREF-USER-TOKEN=$token" + WriteLog "Retrieved Lenovo PSREF token: $token" + + # Clean up. + $socket.Dispose() + + if ($null -ne $socket) { + $socket.Dispose() + } + + # Find the PID listening on the debugging port for reliable termination. + $listeningPid = $null + try { + # Find the process listening on the specific port. The regex now looks for the local address and port, followed by anything, then LISTENING. + # Dots are escaped for literal matching. + $netstatOutput = netstat -ano -p TCP | Where-Object { $_ -match "127\.0\.0\.1:$port.*LISTENING" } + if ($netstatOutput) { + # The last number in the line is the PID + $listeningPid = ($netstatOutput -split '\s+')[-1] + WriteLog "Found Edge process PID $listeningPid listening on port $port. This is the process we will terminate." + } + else { + WriteLog "Could not find any process listening on port $port." + } + } + catch { + WriteLog "Could not run netstat to find listening PID. Error: $($_.Exception.Message)" + } + + # Determine the correct PID to kill. Prioritize the one found via netstat. + $pidToKill = $null + if ($listeningPid) { + $pidToKill = $listeningPid + } + elseif ($null -ne $edgeProcess -and -not $edgeProcess.HasExited) { + $pidToKill = $edgeProcess.Id + WriteLog "Could not find listening process via netstat. Falling back to initial Edge process PID $($pidToKill) for termination." + } + + if ($pidToKill) { + WriteLog "Attempting to terminate Edge process tree with PID: $pidToKill" + try { + taskkill /PID $pidToKill /T /F | Out-Null + WriteLog "Successfully issued termination command for Edge process tree with PID: $pidToKill." + } + catch { + WriteLog "Failed to terminate Edge process tree with PID: $pidToKill. It may have already closed. Error: $($_.Exception.Message)" + } + } + else { + WriteLog "No active Edge process found to terminate." + } + + return $token +} + # -------------------------------------------------------------------------- # SECTION: Module Export # -------------------------------------------------------------------------- -Export-ModuleMember -Function Compress-DriverFolderToWim, Update-DriverMappingJson, Test-ExistingDriver \ No newline at end of file +Export-ModuleMember -Function Compress-DriverFolderToWim, Update-DriverMappingJson, Test-ExistingDriver, Get-LenovoPSREFToken \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 index c3de3b6..cc46f5c 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Lenovo.psm1 @@ -17,6 +17,20 @@ function Get-LenovoDriversModelList { [string]$UserAgent ) + # Lenovo is special - they prevent access to the PSREF API without a cookie as of July 2025. + # This cookie must be retrieved via Javascript + # It appears that the cookie is hard-coded. We'll see how long this lasts. + # If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know. + # https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models). + + # $lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg" + + # Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes. + $lenovoCookie = Get-LenovoPSREFToken + + # Add the cookie to the headers + $Headers["Cookie"] = $lenovoCookie + WriteLog "Querying Lenovo PSREF API for model/machine type: $ModelSearchTerm" $url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($ModelSearchTerm))" $models = [System.Collections.Generic.List[PSCustomObject]]::new() diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 index bb57081..70c497d 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.psm1 @@ -15,7 +15,7 @@ $script:Headers = @{ "Accept-Encoding" = "gzip, deflate, br, zstd" "Accept-Language" = "en-US,en;q=0.9" "Priority" = "u=0, i" - "Sec-Ch-Ua" = "`"Microsoft Edge`";v=`"125`", `"Chromium`";v=`"125`", `"Not.A/Brand`";v=`"24`"" + "Sec-Ch-Ua" = "`"Not)A;Brand`";v=`"8`", `"Chromium`";v=`"138`", `"Microsoft Edge`";v=`"138`"" "Sec-Ch-Ua-Mobile" = "?0" "Sec-Ch-Ua-Platform" = "`"Windows`"" "Sec-Fetch-Dest" = "document" @@ -24,7 +24,7 @@ $script:Headers = @{ "Sec-Fetch-User" = "?1" "Upgrade-Insecure-Requests" = "1" } -$script:UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0' +$script:UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0' function Get-CoreStaticVariables { [CmdletBinding()]