Skip to content

Commit

Permalink
Use our GUID for the session-id and remove the need for initial JS ex…
Browse files Browse the repository at this point in the history
…ecution

* Credits go to whatever127 for the suggestion
* Also ensure that the "Please wait..." message displays for all server queries
* Closes #6
  • Loading branch information
pbatard committed Mar 16, 2019
1 parent 54a8edd commit 5f246fc
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 97 deletions.
74 changes: 9 additions & 65 deletions Fido.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Fido v1.05 - Retail Windows ISO Downloader
# Fido v1.06 - Retail Windows ISO Downloader
# Copyright © 2019 Pete Batard <[email protected]>
# ConvertTo-ImageSource: Copyright © 2016 Chris Carter
#
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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\"
Expand Down Expand Up @@ -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]) {
Expand All @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
38 changes: 6 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
------------
Expand All @@ -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!

0 comments on commit 5f246fc

Please sign in to comment.