Created
October 16, 2025 13:24
-
-
Save jiripolasek/79ea55c8c80651e15586cef24b77405f to your computer and use it in GitHub Desktop.
Detects effective default browser using Windows API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| Get-ProtocolAssoc.ps1 | |
| Queries Windows for the effective handler of http/https using AssocQueryString. | |
| Compatible with Windows PowerShell 5.1 and PowerShell 7+. | |
| OUTPUT | |
| - Prints each field for http and https | |
| - Returns a PSCustomObject you can pipe to ConvertTo-Json | |
| USAGE | |
| powershell -NoProfile -ExecutionPolicy Bypass -File .\Get-ProtocolAssoc.ps1 | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [string[]]$Protocols = @('http','https'), | |
| [string]$Verb = 'open' | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| function Write-Section { | |
| param([string]$Title) | |
| Write-Host "`n####################### $Title #######################" -ForegroundColor Yellow | |
| } | |
| function Write-Field { | |
| param([string]$Name, [string]$Value) | |
| if ([string]::IsNullOrEmpty($Value)) { $Value = '<null or empty>' } | |
| Write-Host ("{0}: {1}" -f $Name, $Value) | |
| } | |
| # ---- P/Invoke wrapper for AssocQueryStringW ---- | |
| if (-not ([System.Management.Automation.PSTypeName]'AssocNative').Type) { | |
| Add-Type -Language CSharp -TypeDefinition @' | |
| using System; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| public static class AssocNative | |
| { | |
| [Flags] | |
| public enum AssocF : uint | |
| { | |
| None = 0x00000000, | |
| Init_NoRemapCLSID = 0x00000001, | |
| Verify = 0x00000040, | |
| NoUserSettings = 0x00000010, | |
| IgnoreBaseClass = 0x00000020 | |
| } | |
| public enum AssocStr : uint | |
| { | |
| Command = 1, | |
| Executable = 2, | |
| FriendlyDocName = 3, | |
| FriendlyAppName = 4, | |
| NoOpen = 5, | |
| ShellNewValue = 6, | |
| DDECommand = 7, | |
| DDEApplication = 8, | |
| DDEIfExec = 9, | |
| DDETopic = 10, | |
| InfoTip = 11, | |
| QuickTip = 12, | |
| TileInfo = 13, | |
| ContentType = 14, | |
| DefaultIcon = 15, | |
| ShellExtension = 16, | |
| DropTarget = 17, | |
| DelegateExecute = 18, | |
| SupportedURISchemes = 19, | |
| ProgID = 20, | |
| AppID = 21, // Some SDKs list AppID around here; value can vary by SDK. We handle failure gracefully. | |
| // Note: Different Windows SDKs have additional values; querying unknown ones may fail, which we catch. | |
| } | |
| [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true, EntryPoint = "AssocQueryStringW")] | |
| public static extern int AssocQueryString( | |
| AssocF flags, | |
| AssocStr str, | |
| string pszAssoc, | |
| string pszExtra, | |
| StringBuilder pszOut, | |
| ref uint pcchOut | |
| ); | |
| } | |
| '@ | |
| } | |
| function Invoke-AssocQuery { | |
| param( | |
| [AssocNative+AssocF]$Flags, | |
| [AssocNative+AssocStr]$What, | |
| [Parameter(Mandatory)][string]$Assoc, # e.g., "http" | |
| [string]$Verb = 'open' | |
| ) | |
| # First call to get length | |
| [uint32]$len = 0 | |
| [void][AssocNative]::AssocQueryString($Flags, $What, $Assoc, $Verb, $null, [ref]$len) | |
| if ($len -eq 0) { return $null } # Not available | |
| $sb = New-Object System.Text.StringBuilder ([int]$len) | |
| $hr = [AssocNative]::AssocQueryString($Flags, $What, $Assoc, $Verb, $sb, [ref]$len) | |
| if ($hr -ne 0) { return $null } # Failure => treat as not available | |
| # Some APIs include an extra null; trim it. | |
| $val = $sb.ToString().TrimEnd([char]0) | |
| if ([string]::IsNullOrWhiteSpace($val)) { return $null } | |
| $val | |
| } | |
| # Combine a couple of useful flags: | |
| # - Verify: ensure returned data maps to something real when possible | |
| # - Init_NoRemapCLSID: avoid remapping through class IDs | |
| $AssocFlags = [AssocNative+AssocF]::Verify -bor [AssocNative+AssocF]::Init_NoRemapCLSID | |
| function Get-ProtocolAssoc { | |
| param([Parameter(Mandatory)][string]$Protocol, [string]$Verb = 'open') | |
| $obj = [ordered]@{ | |
| Protocol = $Protocol | |
| Verb = $Verb | |
| Command = $null | |
| Executable = $null | |
| FriendlyAppName = $null | |
| AppID = $null | |
| DefaultIcon = $null | |
| } | |
| $obj.Command = Invoke-AssocQuery -Flags $AssocFlags -What ([AssocNative+AssocStr]::Command) -Assoc $Protocol -Verb $Verb | |
| $obj.Executable = Invoke-AssocQuery -Flags $AssocFlags -What ([AssocNative+AssocStr]::Executable) -Assoc $Protocol -Verb $Verb | |
| $obj.FriendlyAppName = Invoke-AssocQuery -Flags $AssocFlags -What ([AssocNative+AssocStr]::FriendlyAppName)-Assoc $Protocol -Verb $Verb | |
| # APPID is not guaranteed on all systems/SDKs; if query fails, we'll just leave it null. | |
| try { $obj.AppID = Invoke-AssocQuery -Flags $AssocFlags -What ([AssocNative+AssocStr]::AppID) -Assoc $Protocol -Verb $Verb } catch {} | |
| $obj.DefaultIcon = Invoke-AssocQuery -Flags $AssocFlags -What ([AssocNative+AssocStr]::DefaultIcon) -Assoc $Protocol -Verb $Verb | |
| [pscustomobject]$obj | |
| } | |
| $results = @() | |
| foreach ($p in $Protocols) { | |
| Write-Section $p | |
| $info = Get-ProtocolAssoc -Protocol $p -Verb $Verb | |
| Write-Field 'Command' $info.Command | |
| Write-Field 'Executable' $info.Executable | |
| Write-Field 'FriendlyAppName' $info.FriendlyAppName | |
| Write-Field 'AppID' $info.AppID | |
| Write-Field 'DefaultIcon' $info.DefaultIcon | |
| $results += $info | |
| } | |
| # Emit structured results (pipe to ConvertTo-Json if desired) | |
| $results |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment