From 5f246fc1c12388bf247e09ea55e65f621e5c4339 Mon Sep 17 00:00:00 2001 From: Pete Batard Date: Sat, 16 Mar 2019 17:13:05 +0000 Subject: [PATCH] Use our GUID for the session-id and remove the need for initial JS execution * Credits go to whatever127 for the suggestion * Also ensure that the "Please wait..." message displays for all server queries * Closes #6 --- Fido.ps1 | 74 +++++++------------------------------------------------ README.md | 38 +++++----------------------- 2 files changed, 15 insertions(+), 97 deletions(-) diff --git a/Fido.ps1 b/Fido.ps1 index 53b48eb..3be65e9 100644 --- a/Fido.ps1 +++ b/Fido.ps1 @@ -1,5 +1,5 @@ # -# Fido v1.05 - Retail Windows ISO Downloader +# Fido v1.06 - Retail Windows ISO Downloader # Copyright © 2019 Pete Batard # ConvertTo-ImageSource: Copyright © 2016 Chris Carter # @@ -44,28 +44,11 @@ Write-Host Please Wait... #region Assembly Types $code = @" -[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - internal static extern IntPtr LoadLibrary(string lpLibFileName); -[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] - internal static extern int LoadString(IntPtr hInstance, uint wID, StringBuilder lpBuffer, int nBufferMax); [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] internal static extern int ExtractIconEx(string sFile, int iIndex, out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr handle, int state); - // Returns a localized MUI string from the specified DLL - public static string GetMuiString(string dll, uint index) - { - int MAX_PATH = 255; - string muiPath = Environment.SystemDirectory + @"\" + CultureInfo.CurrentUICulture.Name + @"\" + dll + ".mui"; - if (!File.Exists(muiPath)) - muiPath = Environment.SystemDirectory + @"\en-US\" + dll + ".mui"; - IntPtr hMui = LoadLibrary(muiPath); - StringBuilder szString = new StringBuilder(MAX_PATH); - LoadString(hMui, (uint)index, szString, MAX_PATH); - return szString.ToString(); - } - // Extract an icon from a DLL public static Icon ExtractIcon(string file, int number, bool largeIcon) { @@ -425,7 +408,7 @@ $ErrorActionPreference = "Stop" $dh = 58; $Stage = 0 $MaxStage = 4 -$SessionId = "" +$SessionId = [guid]::NewGuid() $ExitCode = 100 $Locale = "en-US" $DFRCKey = "HKLM:\Software\Policies\Microsoft\Internet Explorer\Main\" @@ -528,31 +511,7 @@ $Continue.add_click({ switch ($Stage) { - 1 { # Windows Version selection => Get a Session ID and populate Windows Release - $XMLForm.Title = Get-Translation($English[12]) - Refresh-Control($XMLForm) - - $url = "https://www.microsoft.com/" + $QueryLocale + "/software-download/" - $url += $WindowsVersion.SelectedValue.PageType - Write-Host Querying $url - - try { - # Note: We can't use -UseBasicParsing since we need JS to create the session-id - # TODO: Use -Headers @{"Cache-Control"="no-cache"}? - $r = Invoke-WebRequest -UserAgent $UserAgent -SessionVariable "Session" $url - $script:SessionId = $(GetElementById -Request $r -Id "session-id").Value - if (-not $SessionId) { - $ErrorMessage = $(GetElementById -Request $r -Id "errorModalMessage").InnerText - if ($ErrorMessage) { - Write-Host "$(Get-Translation("Error")): ""$ErrorMessage""" - } - throw "Could not read Session ID" - } - } catch { - Error($_.Exception.Message) - return - } - + 1 { # Windows Version selection $i = 0 $array = @() foreach ($Version in $WindowsVersions[$WindowsVersion.SelectedValue.Index]) { @@ -564,7 +523,6 @@ $Continue.add_click({ $script:WindowsRelease = Add-Entry $Stage "Release" $array $Back.Content = Get-Translation($English[8]) - $XMLForm.Title = $AppTitle } 2 { # Windows Release selection => Populate Product Edition @@ -581,6 +539,8 @@ $Continue.add_click({ } 3 { # Product Edition selection => Request and populate Languages + $XMLForm.Title = Get-Translation($English[12]) + Refresh-Control($XMLForm) $url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html" $url += "?pageId=" + $RequestData["GetLangs"][0] $url += "&host=www.microsoft.com" @@ -628,9 +588,12 @@ $Continue.add_click({ } $script:Language = Add-Entry $Stage "Language" $array "DisplayLanguage" $Language.SelectedIndex = $SelectedIndex + $XMLForm.Title = $AppTitle } 4 { # Language selection => Request and populate Arch download links + $XMLForm.Title = Get-Translation($English[12]) + Refresh-Control($XMLForm) $url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html" $url += "?pageId=" + $RequestData["GetLinks"][0] $url += "&host=www.microsoft.com" @@ -712,6 +675,7 @@ $Continue.add_click({ } $Arch.SelectedIndex = $SelectedIndex $Continue.Content = Get-Translation("Download") + $XMLForm.Title = $AppTitle } 5 { # Arch selection => Return selected download link @@ -759,31 +723,11 @@ $Back.add_click({ } }) -if (-not $PipeName) { - # We need a job in the background to close the obnoxious "Do you want to accept this cookie?" Windows alerts - $ClosePrompt = { - param($PromptTitle) - while ($True) { - Get-Process | Where-Object { $_.MainWindowTitle -match $PromptTitle } | ForEach-Object { $_.CloseMainWindow() } - Start-Sleep -Milliseconds 100 - } - } - # Get the localized version of the 'Windows Security Warning' title of the cookie prompt - $SecurityWarningTitle = [Gui.Utils]::GetMuiString("urlmon.dll", 2070) - if (-not $SecurityWarningTitle) { - $SecurityWarningTitle = "Windows Security Warning" - } - $Job = Start-Job -ScriptBlock $ClosePrompt -ArgumentList $SecurityWarningTitle -} - # Display the dialog $XMLForm.Add_Loaded( { $XMLForm.Activate() } ) $XMLForm.ShowDialog() | Out-Null # Clean up & exit -if (-not $PipeName) { - Stop-Job -Job $Job -} if ($DFRCAdded) { Remove-ItemProperty -Path $DFRCKey -Name $DFRCName } diff --git a/README.md b/README.md index a17ec7d..4918164 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Fido is a PowerShell script that is primarily designed to be used in [Rufus](htt can also be used in standalone fashion, and that automates access to the official Windows retail ISO download links. We decided to create this script because, while Microsoft does make retail ISO download links freely and publicly -available on their website (at least for Windows 8 and Windows 10), it only does so after actively forcing users to jump -through a lot of unwarranted hoops, that create an exceedingly counterproductive, if not downright unfriendly, +available on their website (at least for Windows 8 and Windows 10), it only does so after actively forcing users to +jump through a lot of unwarranted hoops, that create an exceedingly counterproductive, if not downright unfriendly, consumer experience, which greatly detracts from what people really want (direct access to ISO downloads). As to the reason one might want to download Windows __retail__ ISOs, as opposed to the ISOs that can be generated by @@ -44,14 +44,10 @@ redirect you __away__ from the pages that allow you to download retail ISOs): * https://www.microsoft.com/software-download/Windows8ISO * https://www.microsoft.com/software-download/Windows10ISO -From visiting those with a full browser (Internet Explorer, running through the `Invoke-WebRequest` PowerShell Cmdlet), -the script then obtains a `session-id` which it can then use to query web APIs on the Microsoft servers to first request -the language selection available for the for the version of Windows that was selected, and then the download links for -the various architecture enabled for that version + language combination. - -As to why a full browser is required, the reason behind that is that the JavaScript from the Microsoft pages does need -to execute before we can access the `session-id`, and PowerShell + `Invoke-WebRequest` is the most flexible, universal -and lightweight way to get that to run, without having to install a bunch of non-native dependencies. +After visiting those with a full browser (Internet Explorer, running through the `Invoke-WebRequest` PowerShell Cmdlet), +to confirm that they are accessible queries web APIs on the Microsoft servers to first request the language selection +available for the for the version of Windows that was selected, and then the download links for the various architecture +enabled for that version + language combination. Requirements ------------ @@ -67,25 +63,3 @@ make sure that you manually launch IE at least once and complete the setup. Note that, if running this script elevated, this annoyance can be avoided by using the `-DisableFirstRunCustomize` option (which basically __temporarily__ creates the key of the same name in the registry __if__ it doesn't already exist, to bypass that behaviour). - -Additional information ----------------------- - -As mentioned earlier, because we need to execute JavaScript (to obtain a `session-id`), "dumb" calls cannot be used -to query the Microsoft servers. This is why we can't use `-UseBasicParsing` with `Invoke-WebRequest` as this option -would remove all JavaScript execution. - -Also, because we are really using IE behind the scenes, the PowerShell script does create a few of Windows Security -Alerts regarding the creation of cookies, which you may see flash. And since it is not possible to tell -`Invoke-WebRequest` to accept or refuse cookies altogether, we must run a second process in the background that -detects and close these alerts automatically. - -Finally, you should be mindful that, since Microsoft __really__ does not appear to like having legitimate customers -trying to download their retail ISOs, they are using deep fingerprinting technology to prevent repeat downloads... -As such, if you request a few too many downloads (3 or 4 in the space of an hour or so), you may get a message about -being temporarily banned. This temporary ban is usually reset within 12-24 hours (or, if you're lucky, it might also -be reset if you switch IP). __However__ you do want to be cautious about triggering this ban a few too many times, -as it appears that Microsoft are using the JavaScript to uniquely fingerprint a specific browser-engine + machine -combination (and, as far as I can tell, this fingerprinting is based on more than cookies + cache data + User-Agent + -IP/MAC address) and if they detect that you have triggered the temporary ban to many times with the script, they -may enact a permanent ban)... You have been warned!