mirror of
https://github.com/pbatard/Fido.git
synced 2025-09-16 22:28:02 +02:00
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
This commit is contained in:
74
Fido.ps1
74
Fido.ps1
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Fido v1.05 - Retail Windows ISO Downloader
|
# Fido v1.06 - Retail Windows ISO Downloader
|
||||||
# Copyright © 2019 Pete Batard <pete@akeo.ie>
|
# Copyright © 2019 Pete Batard <pete@akeo.ie>
|
||||||
# ConvertTo-ImageSource: Copyright © 2016 Chris Carter
|
# ConvertTo-ImageSource: Copyright © 2016 Chris Carter
|
||||||
#
|
#
|
||||||
@@ -44,28 +44,11 @@ Write-Host Please Wait...
|
|||||||
|
|
||||||
#region Assembly Types
|
#region Assembly Types
|
||||||
$code = @"
|
$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)]
|
[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);
|
internal static extern int ExtractIconEx(string sFile, int iIndex, out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons);
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern bool ShowWindow(IntPtr handle, int state);
|
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
|
// Extract an icon from a DLL
|
||||||
public static Icon ExtractIcon(string file, int number, bool largeIcon)
|
public static Icon ExtractIcon(string file, int number, bool largeIcon)
|
||||||
{
|
{
|
||||||
@@ -425,7 +408,7 @@ $ErrorActionPreference = "Stop"
|
|||||||
$dh = 58;
|
$dh = 58;
|
||||||
$Stage = 0
|
$Stage = 0
|
||||||
$MaxStage = 4
|
$MaxStage = 4
|
||||||
$SessionId = ""
|
$SessionId = [guid]::NewGuid()
|
||||||
$ExitCode = 100
|
$ExitCode = 100
|
||||||
$Locale = "en-US"
|
$Locale = "en-US"
|
||||||
$DFRCKey = "HKLM:\Software\Policies\Microsoft\Internet Explorer\Main\"
|
$DFRCKey = "HKLM:\Software\Policies\Microsoft\Internet Explorer\Main\"
|
||||||
@@ -528,31 +511,7 @@ $Continue.add_click({
|
|||||||
|
|
||||||
switch ($Stage) {
|
switch ($Stage) {
|
||||||
|
|
||||||
1 { # Windows Version selection => Get a Session ID and populate Windows Release
|
1 { # Windows Version selection
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
$i = 0
|
$i = 0
|
||||||
$array = @()
|
$array = @()
|
||||||
foreach ($Version in $WindowsVersions[$WindowsVersion.SelectedValue.Index]) {
|
foreach ($Version in $WindowsVersions[$WindowsVersion.SelectedValue.Index]) {
|
||||||
@@ -564,7 +523,6 @@ $Continue.add_click({
|
|||||||
|
|
||||||
$script:WindowsRelease = Add-Entry $Stage "Release" $array
|
$script:WindowsRelease = Add-Entry $Stage "Release" $array
|
||||||
$Back.Content = Get-Translation($English[8])
|
$Back.Content = Get-Translation($English[8])
|
||||||
$XMLForm.Title = $AppTitle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
2 { # Windows Release selection => Populate Product Edition
|
2 { # Windows Release selection => Populate Product Edition
|
||||||
@@ -581,6 +539,8 @@ $Continue.add_click({
|
|||||||
}
|
}
|
||||||
|
|
||||||
3 { # Product Edition selection => Request and populate Languages
|
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 = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
|
||||||
$url += "?pageId=" + $RequestData["GetLangs"][0]
|
$url += "?pageId=" + $RequestData["GetLangs"][0]
|
||||||
$url += "&host=www.microsoft.com"
|
$url += "&host=www.microsoft.com"
|
||||||
@@ -628,9 +588,12 @@ $Continue.add_click({
|
|||||||
}
|
}
|
||||||
$script:Language = Add-Entry $Stage "Language" $array "DisplayLanguage"
|
$script:Language = Add-Entry $Stage "Language" $array "DisplayLanguage"
|
||||||
$Language.SelectedIndex = $SelectedIndex
|
$Language.SelectedIndex = $SelectedIndex
|
||||||
|
$XMLForm.Title = $AppTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
4 { # Language selection => Request and populate Arch download links
|
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 = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
|
||||||
$url += "?pageId=" + $RequestData["GetLinks"][0]
|
$url += "?pageId=" + $RequestData["GetLinks"][0]
|
||||||
$url += "&host=www.microsoft.com"
|
$url += "&host=www.microsoft.com"
|
||||||
@@ -712,6 +675,7 @@ $Continue.add_click({
|
|||||||
}
|
}
|
||||||
$Arch.SelectedIndex = $SelectedIndex
|
$Arch.SelectedIndex = $SelectedIndex
|
||||||
$Continue.Content = Get-Translation("Download")
|
$Continue.Content = Get-Translation("Download")
|
||||||
|
$XMLForm.Title = $AppTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
5 { # Arch selection => Return selected download link
|
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
|
# Display the dialog
|
||||||
$XMLForm.Add_Loaded( { $XMLForm.Activate() } )
|
$XMLForm.Add_Loaded( { $XMLForm.Activate() } )
|
||||||
$XMLForm.ShowDialog() | Out-Null
|
$XMLForm.ShowDialog() | Out-Null
|
||||||
|
|
||||||
# Clean up & exit
|
# Clean up & exit
|
||||||
if (-not $PipeName) {
|
|
||||||
Stop-Job -Job $Job
|
|
||||||
}
|
|
||||||
if ($DFRCAdded) {
|
if ($DFRCAdded) {
|
||||||
Remove-ItemProperty -Path $DFRCKey -Name $DFRCName
|
Remove-ItemProperty -Path $DFRCKey -Name $DFRCName
|
||||||
}
|
}
|
||||||
|
38
README.md
38
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.
|
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
|
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
|
available on their website (at least for Windows 8 and Windows 10), it only does so after actively forcing users to
|
||||||
through a lot of unwarranted hoops, that create an exceedingly counterproductive, if not downright unfriendly,
|
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).
|
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
|
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/Windows8ISO
|
||||||
* https://www.microsoft.com/software-download/Windows10ISO
|
* https://www.microsoft.com/software-download/Windows10ISO
|
||||||
|
|
||||||
From visiting those with a full browser (Internet Explorer, running through the `Invoke-WebRequest` PowerShell Cmdlet),
|
After 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
|
to confirm that they are accessible queries web APIs on the Microsoft servers to first request the language selection
|
||||||
the language selection available for the for the version of Windows that was selected, and then the download links for
|
available for the for the version of Windows that was selected, and then the download links for the various architecture
|
||||||
the various architecture enabled for that version + language combination.
|
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.
|
|
||||||
|
|
||||||
Requirements
|
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`
|
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
|
option (which basically __temporarily__ creates the key of the same name in the registry __if__ it doesn't already
|
||||||
exist, to bypass that behaviour).
|
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!
|
|
||||||
|
Reference in New Issue
Block a user