src/web.ps1
class Options { [String[]] GetProperties() { return $this | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name } [PSObject] SetProperties($Object) { $this.GetProperties() | ForEach-Object { $Object.$_ = $this.$_ } return $Object } } class FormOptions: Options { [Int] $Width = 960 [Int] $Height = 700 [Int] $FormBorderStyle = 3 [Double] $Opacity = 1.0 [Bool] $ControlBox = $True [Bool] $MaximizeBox = $False [Bool] $MinimizeBox = $False } class BrowserOptions: Options { [String] $Anchor = 'Left,Top,Right,Bottom' [PSObject] $Size = @{ Height = 700; Width = 960 } [Bool] $IsWebBrowserContextMenuEnabled = $False } function Add-Metadata { <# .SYNOPSIS Identify certain elements and wrap them in semantic HTML tags. .EXAMPLE 'My email is foo@bar.com' | ConvertTo-Html #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [String] $Text, [String[]] $Keyword, [Hashtable] $Abbreviations, [Switch] $Microformat, [ValidateSet('all', 'date', 'duration', 'email', 'url', 'ip')] [String[]] $Disable ) Begin { $Custom = [Regex](($Keyword | ForEach-Object { "(\b${_}\b)" }) -join '|' ) $Date = [Regex](New-RegexString -Date) $Duration = [RegEx](New-RegexString -Duration) $Email = [Regex](New-RegexString -Email) $Url = [Regex](New-RegexString -Url) $IpAdress = [Regex](New-RegexString -IPv4 -IPv6) $Attributes = @{ Custom = 'itemprop="thing"' Date = 'itemscope itemtype="https://schema.org/DateTime" class="dt-event"' Duration = 'itemscope itemprop="event" itemtype="https://schema.org/Event" class="duration dt-event"' Email = 'itemscope itemprop="email" itemtype="https://schema.org/email" class="u-email"' End = 'itemscope itemprop="endTime" itemtype="https://schema.org/Time" class="dt-end"' Start = 'itemscope itemprop="startTime" itemtype="https://schema.org/Time" class="dt-start"' Url = 'itemscope itemprop="url" itemtype="https://schema.org/URL" class="u-url"' } $Options = [Text.RegularExpressions.RegexOptions]'IgnoreCase, CultureInvariant' } Process { If ($Keyword.Count -gt 0) { $Text = [Regex]::Replace( $Text, $Custom, { Param($Match) $Value = $Match.Value $ClassName = $Value -replace '\s', '-' if ($Microformat) { "<span $($Attributes.Custom) class=`"keyword p-item`" data-keyword=`"${ClassName}`">${Value}</span>" } else { "<span class=`"keyword`" data-keyword=`"${ClassName}`">${Value}</span>" } } ) } if ($Abbreviations.Count -gt 0) { $Items = $Abbreviations.GetEnumerator() foreach ($Item in $Items) { $Name = $Item.Name $Value = $Item.Value $Text = [Regex]::Replace( $Text, "\b${Value}\b", { Param($Match) $Value = $Match.Value "<abbr title=`"${Name}`">${Value}</abbr>" } ) } } if ('all' -notin $Disable) { switch ($True) { { 'url' -notin $Disable } { $Text = [Regex]::Replace( $Text, $Url, { Param($Match) $Value = $Match.Groups[1].Value if ($Microformat) { "<a $($Attributes.Url) href=`"${Value}`">${Value}</a>" } else { "<a href=`"${Value}`">${Value}</a>" } }, $Options ) } { 'date' -notin $Disable } { $Text = [Regex]::Replace( $Text, $Date, { Param($Match) $Value = $Match.Groups[1].value $Data = $Value | Test-Match -Date $IsoValue = [DateTime]"$($Data.Month)/$($Data.Day)/$($Data.Year)" | ConvertTo-Iso8601 if ($Microformat) { "<time $($Attributes.Date) datetime=`"${IsoValue}`">${Value}</time>" } else { "<time datetime=`"${IsoValue}`">${Value}</time>" } }, $Options ) } { 'duration' -notin $Disable } { $Text = [Regex]::Replace( $Text, $Duration, { Param($Match) $Value = $Match.Groups[1].value $Data = $Value | Test-Match -Duration $Start = $Data.Start $End = $Data.End $Timezone = if ($Data.IsZulu) { ' data-timezone="Zulu"' } else { '' } if ($Microformat) { "<span $($Attributes.Duration)${Timezone}><time $($Attributes.Start) datetime=`"${Start}`">${Start}</time> - <time $($Attributes.End) datetime=`"${End}`">${End}</time></span>" } else { "<span class=`"duration`"${Timezone}><time datetime=`"${Start}`">${Start}</time> - <time datetime=`"${End}`">${End}</time></span>" } }, $Options ) } { 'email' -notin $Disable } { $Text = [Regex]::Replace( $Text, $Email, { Param($Match) $Value = $Match.Groups[1].Value if ($Microformat) { "<a $($Attributes.Email) href=`"mailto:${Value}`">${Value}</a>" } else { "<a href=`"mailto:${Value}`">${Value}</a>" } }, $Options ) } { 'ip' -notin $Disable } { $Text = [Regex]::Replace( $Text, $IpAdress, { Param($Match) $Value = $Match.Groups[1].Value "<a class=`"ip`" href=`"${Value}`">${Value}</a>" }, $Options ) } } } $Text } } function ConvertFrom-ByteArray { <# .SYNOPSIS Converts bytes to human-readable text #> [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [Array] $Data ) Begin { function Invoke-Convert { Param( [Parameter(Position = 0)] $Data ) if ($Data.Length -gt 0) { if ($Data -is [Byte] -or $Data[0] -is [Byte]) { [System.Text.Encoding]::ASCII.GetString($Data) } else { $Data } } } Invoke-Convert $Data } End { Invoke-Convert $Input } } function ConvertFrom-Html { <# .SYNOPSIS Convert HTML string into object. .EXAMPLE '<html><body><h1>hello</h1></body></html>' | ConvertFrom-Html #> [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [String] $Value ) $Html = New-Object -ComObject 'HTMLFile' try { # This works in PowerShell with Office installed $Html.IHTMLDocument2_write($Value) } catch { # This works when Office is not installed $Content = [System.Text.Encoding]::Unicode.GetBytes($Value) $Html.Write($Content) } $Html } function ConvertFrom-QueryString { <# .SYNOPSIS Returns parsed query parameters #> [CmdletBinding()] [OutputType([Object[]])] [OutputType([String])] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [String] $Query ) Begin { Use-Web } Process { $Decoded = [System.Web.HttpUtility]::UrlDecode($Query) if ($Decoded -match '=') { $Decoded -split '&' | Invoke-Reduce { Param($Acc, $Item) $Key, $Value = $Item -split '=' $Acc.$Key = $Value.Trim() } -InitialValue @{} } else { $Decoded } } } function ConvertTo-Iso8601 { <# .SYNOPSIS Convert value to date in ISO 8601 format .NOTES See https://www.iso.org/iso-8601-date-and-time-format.html #> [CmdletBinding()] Param( [Parameter(Position = 0, ValueFromPipeline = $True)] [String] $Value ) Process { $Value | Get-Date -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' } } function ConvertTo-JavaScript { <# .SYNOPSIS Convert PowerShell values to JavaScript strings. It is similar to ConvertTo-Json, but with broader support for Prelude types. .EXAMPLE $A = [Node]'A' $B = [Node]'B' $A, $B | ConvertTo-JavaScript .EXAMPLE @{ foo = 'bar' } | ConvertTo-JavaScript # returns {"foo":"bar"} .NOTES The ConvertTo-JavaScript cmdlet is not intended to be used as a data serializer as data is removed during conversion. #> [CmdletBinding()] [OutputType([System.String])] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] $Value ) Begin { $CoordinateTemplate = { Param($Value) "{latitude: $($Value.Latitude), longitude: $($Value.Longitude), height: $($Value.Height), hemisphere: '$($Value.Hemisphere -join '')'}" } $MatrixTemplate = { Param($Value) $Rows = $Value.Values.Real | Invoke-Chunk -Size $Value.Size[1] | ForEach-Object { $_ -join ', ' } | ForEach-Object { "[$_]" } "[$($Rows -join ', ')]" } $NodeTemplate = { Param($Value) "{id: '$($Value.Id)', label: '$($Value.Label)'}" } $EdgeTemplate = { Param($Value) $Source = (& $NodeTemplate -Value $Value.Source) $Target = (& $NodeTemplate -Value $Value.Target) "{source: $Source, target: $Target}" } $GraphTemplate = { Param($Value) "{nodes: $($Value.Nodes | ConvertTo-JavaScript), edges: $($Value.Edges | ConvertTo-JavaScript)}" } $DefaultTemplate = { Param($Value) $Value | ConvertTo-Json -Compress } function Invoke-Convert { Param($Value) $Type = $Value.GetType().Name $Template = switch ($Type) { 'Coordinate' { $CoordinateTemplate } 'Matrix' { $MatrixTemplate } 'Node' { $NodeTemplate } 'DirectedEdge' { $EdgeTemplate } 'Edge' { $EdgeTemplate } 'Graph' { $GraphTemplate } Default { $DefaultTemplate } } & $Template -Value $Value } switch ($Value.Count) { 1 { Invoke-Convert -Value $Value } { $_ -gt 1 } { "[$(($Value | ForEach-Object { Invoke-Convert -Value $_ }) -join ', ')]" } } } End { switch ($Input.Count) { 1 { Invoke-Convert -Value $Input[0] } { $_ -gt 1 } { "[$(($Input | ForEach-Object { Invoke-Convert -Value $_ }) -join ', ')]" } } } } function ConvertTo-QueryString { <# .SYNOPSIS Returns URL-encoded query string #> [CmdletBinding()] [OutputType([String])] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [PSObject] $InputObject, [Switch] $UrlEncode ) Begin { Use-Web } Process { $Callback = { Param($Acc, $Item) $Key = $Item.Name $Value = $Item.Value "${Acc}$(if ($Acc -ne '') { '&' } else { '' })${Key}=${Value}" } $QueryString = $InputObject.GetEnumerator() | Sort-Object Name | Invoke-Reduce $Callback '' if (-not $QueryString) { $QueryString = '' } if ($UrlEncode) { Add-Type -AssemblyName System.Web [System.Web.HttpUtility]::UrlEncode($QueryString) } else { $QueryString } } } function Get-GithubOAuthToken { <# .SYNOPSIS Obtain OAuth token from https://api.github.com .DESCRIPTION This function enables obtaining an OAuth token from https://api.github.com Github provides multiple ways to obtain authentication tokens. This function implements the "device flow" method of authorizing an OAuth app. Before using this function, you must: 1. Create an OAuth app on Github.com 2. Record app "Client ID" (passed as -ClientID) 3. Opt-in to "Device Authorization Flow" beta feature This function will attempt to open a browser and will require the user to login to his/her Github account to authorize access. The one-time device code will be copied to the clipboard for ease of use. > Note: For basic authentication scenarios, please use Invoke-WebRequestBasicAuth .EXAMPLE $Token = Get-GithubOAuthToken -ClientId $ClientId -Scope 'notifications' $Request = BasicAuth $Token -Uri 'https://api.github.com/notifications' #> [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 0)] [String] $ClientId, [Parameter(Position = 1)] [String[]] $Scope ) $ValidScopes = 'repo', 'repo:status', 'repo_deployment', 'public_repo', 'repo:invite', 'security_events', 'admin:repo_hook', 'write:repo_hook', 'read:repo_hook', 'admin:org', 'write:org', 'read:org', 'admin:public_key', 'write:public_key', 'read:public_key', 'admin:org_hook', 'gist', 'notifications', 'user', 'read:user', 'user:email', 'user:follow', 'delete_repo', 'write:discussion', 'read:discussion', 'write:packages', 'read:packages', 'delete:packages', 'admin:gpg_key', 'write:gpg_key', 'read:gpg_key', 'workflow' $IsValidScope = $Scope | Invoke-Reduce { Param($Acc, $Item) $Acc -and ($Item -in $ValidScopes) } if ($IsValidScope) { $DeviceRequestParameters = @{ Post = $True Uri = 'https://github.com/login/device/code' Query = @{ client_id = $ClientId scope = $Scope -join '%20' } } $DeviceData = Invoke-WebRequestBasicAuth @DeviceRequestParameters | ForEach-Object -MemberName 'Content' | ConvertFrom-ByteArray | ConvertFrom-QueryString $DeviceData | ConvertTo-Json | Write-Verbose $TokenRequestParameters = @{ Post = $True Uri = 'https://github.com/login/oauth/access_token' Query = @{ client_id = $ClientId device_code = $DeviceData['device_code'] grant_type = 'urn:ietf:params:oauth:grant-type:device_code' } } $DeviceData['user_code'] | Write-Title -Green -PassThru | Set-Clipboard Start-Process $DeviceData['verification_uri'] $Success = $False while (-not $Success) { Start-Sleep $DeviceData.interval $TokenData = Invoke-WebRequestBasicAuth @TokenRequestParameters | ForEach-Object -MemberName 'Content' | ConvertFrom-ByteArray | ConvertFrom-QueryString $Success = $TokenData['token_type'] -eq 'bearer' } $TokenData | ConvertTo-Json | Write-Verbose $TokenData['access_token'] } else { "One or more scope values are invalid (Scopes: $(Join-StringsWithGrammar $Scope))" | Write-Error } } function Get-HostsContent { <# .SYNOPSIS Get and parse contents of hosts file .PARAMETER Path Specifies an alternate hosts path. Defaults to %SystemRoot%\System32\drivers\etc\hosts. .EXAMPLE Get-HostsContent .EXAMPLE Get-HostsContent '.\hosts' #> [CmdletBinding()] [OutputType([String])] Param( [Parameter(Position = 0, ValueFromPipeline = $True)] [ValidateScript( { Test-Path $_ })] [String] $Path ) if (-not $Path) { $Path = if (-not $IsLinux) { Join-Path $Env:SystemRoot 'System32\drivers\etc\hosts' } else { '/etc/hosts' } } $CommentLine = '^\s*#' $HostLine = '^\s*(?<IPAddress>\S+)\s+(?<Hostname>\S+)(\s*|\s+#(?<Comment>.*))$' $HomeAddress = [Net.IPAddress]'127.0.0.1' $LineNumber = 0 (Get-Content $Path -ErrorAction Stop) | ForEach-Object { if (($_ -match $HostLine) -and ($_ -notmatch $CommentLine)) { $IpAddress = $Matches['IPAddress'] $Comment = if ($Matches['Comment']) { $Matches['Comment'] } else { '' } $Result = [PSCustomObject]@{ LineNumber = $LineNumber IPAddress = $IpAddress IsValidIP = [Net.IPAddress]::TryParse($IPAddress, [Ref] $HomeAddress) Hostname = $Matches['Hostname'] Comment = $Comment.Trim() } $Result.PSObject.TypeNames.Insert(0, 'Hosts.Entry') $Result } $LineNumber++ } } function Get-HtmlElement { <# .SYNOPSIS Helper utility for getting elements as an array from HTML formatted input using tagname, id, or class name .EXAMPLE $Html | Get-HtmlElement 'div' .EXAMPLE $Html | Get-HtmlElement '.some-class' .EXAMPLE $Html | Get-HtmlElement '#some-identifier' #> [CmdletBinding()] [OutputType([System.Object[]])] Param( [Parameter(ValueFromPipeline = $True)] $InputObject, [Parameter(Mandatory = $True, Position = 0)] [String] $Selector ) Process { $InputType = $InputObject.GetType().Name $Html = if ($InputType -eq 'String') { $InputObject | ConvertFrom-Html } else { $InputObject } $Elements = @() switch -Regex ($Selector) { '^[.].*' { $ClassName = $_ | Remove-Character -At 0 foreach ($Element in $Html.all) { if ($Element.className -eq $ClassName) { $Elements += $Element } } } '^#.*' { $Id = $_ | Remove-Character -At 0 foreach ($Element in $Html.getElementById($Id)) { $Elements += $Element } } Default { foreach ($Element in $Html.all.tags($Selector)) { $Elements += $Element } } } $Elements } } function Import-Html { <# .SYNOPSIS Import and parse an a local HTML file or web page. .EXAMPLE Import-Html example.com | ForEach-Object { $_.body.innerHTML } .EXAMPLE Import-Html .\bookmarks.html | ForEach-Object { $_.all.tags('a') } | Selelct-Object -ExpandProperty textContent #> [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [Alias('Uri')] [String] $Path ) if (Test-Path $Path) { $Content = Get-Content -Path $Path -Raw } else { $Content = (Invoke-WebRequest -Uri $Path).Content } ConvertFrom-Html $Content } function Invoke-WebRequestBasicAuth { <# .SYNOPSIS Invoke-WebRequest wrapper that makes it easier to use basic authentication .PARAMETER TwoFactorAuthentication Name of API that requires 2FA. Use 'none' when 2FA is not required. Possible values: - 'Github' - 'none' [Default] .PARAMETER Data Data (Body) payload for HTTP request. Will only function with PUT and POST requests. ==> Analogous to the '-d' cURL flag ==> Data object will be converted to JSON string .PARAMETER WebRequestParameters Object for passing parameters to underlying invocation of Invoke-WebRequest .EXAMPLE # Authenticate a GET request with a token $Uri = 'https://api.github.com/notifications' $Query = @{ per_page = 100; page = 1 } $Request = Invoke-WebRequestBasicAuth $Token -Uri $Uri -Query $Query $Request.Content | ConvertFrom-Json | Format-Table -AutoSize .EXAMPLE # Use basic authentication with a username and password $Uri = 'https://api.github.com/notifications' $Query = @{ per_page = 100; page = 1 } $Request = Invoke-WebRequestBasicAuth $Username -Password $Token -Uri $Uri -Query $Query $Request.Content | ConvertFrom-Json | Format-Table -AutoSize .EXAMPLE # Execute a PUT request with a data payload $Uri = 'https://api.github.com/notifications' @{ last_read_at = '' } | BasicAuth $Token -Uri $Uri -Put .EXAMPLE $Uri = 'https://api.github.com/notifications' $Parameters = @{ SkipCertificateChecks = $True } @{ last_read_at = '' } | BasicAuth $Token -Uri $Uri -Put -RequestParameters $Parameters #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Password')] [CmdletBinding(DefaultParameterSetName = 'token')] [Alias('basicauth')] Param( [Parameter(ParameterSetName = 'basic', Position = 0)] [String] $Username, [Parameter(ParameterSetName = 'basic')] [String] $Password, [Parameter(ParameterSetName = 'token', Position = 0)] [String] $Token, [Parameter(Mandatory = $True)] [UriBuilder] $Uri, [PSObject] $Query = @{}, [Switch] $UrlEncode, [Switch] $ParseContent, [Alias('OTP')] [String] $TwoFactorAuthentication = 'none', [Switch] $Get, [Switch] $Post, [Switch] $Put, [Switch] $Delete, [Parameter(ValueFromPipeline = $True)] [PSObject] $Data = @{}, [PSObject] $WebRequestParameters = @{} ) Process { $Authorization = if ($Token.Length -gt 0) { "Bearer $Token" } else { $Credential = [Convert]::ToBase64String([System.Text.Encoding]::Ascii.GetBytes("${Username}:${Password}")) "Basic $Credential" } $Headers = @{ Authorization = $Authorization } switch ($TwoFactorAuthentication) { 'github' { 'GitHub 2FA' | Write-Title -Green $Code = 'Code:' | Invoke-Input -Number -Indent 4 $Headers.Accept = 'application/vnd.github.v3+json' $Headers['x-github-otp'] = $Code } Default { # Do nothing } } $Method = Find-FirstTrueVariable 'Get', 'Post', 'Put', 'Delete' $Uri.Query = $Query | ConvertTo-QueryString -UrlEncode:$UrlEncode $Parameters = @{ Headers = $Headers Method = $Method Uri = $Uri.Uri } "==> Headers: $($Parameters.Headers | ConvertTo-Json)" | Write-Verbose "==> Method: $($Parameters.Method)" | Write-Verbose "==> URI: $($Parameters.Uri)" | Write-Verbose if ($Method -in 'Post', 'Put') { $Parameters.Body = $Data | ConvertTo-Json "==> Data: $($Data | ConvertTo-Json)" | Write-Verbose } $Request = Invoke-WebRequest @Parameters @WebRequestParameters if ($ParseContent) { $Request.Content | ConvertFrom-Json } else { $Request } } } function Out-Browser { <# .SYNOPSIS Display HTML content (string, file, or URI) in a web browser Windows form. Out-Browser will auto-detect content type. Returns [System.Windows.Forms.HtmlDocument] object .PARAMETER OnShown Function to be executed once form is shown. $Form and $Browser variables are available within function scope. .PARAMETER OnComplete Function to be executed whenever the Document within the browser is loaded. $Form and $Browser variables are available within function scope. .PARAMETER OnClose Function to be executed immediately before form is disposed. $Form and $Browser variables are available within function scope. .PARAMETER Default Use operating system default browser (i.e. Firefox, Chrome, etc...) instead of WebBrowser control. Note: OnShown, OnComplete, and OnClose will not be run when the Default parameter is used. .PARAMETER PassThru - Return WebBrowser document object when not using -Default parameter - Return process when using -Default parameter .EXAMPLE '<h1>Hello World</h1>' | Out-Browser .EXAMPLE 'https://google.com' | Out-Browser .EXAMPLE '.\file.html' | Out-Browser .EXAMPLE $OnClose = { Param($Browser) $Browser.Document.GetElementsByTagName('h1').innerText | Write-Color -Green } '<h1 contenteditable="true">Type Here</h1>' | Out-Browser -OnClose $OnClose | Out-Null #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function')] [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [String] $Content, [FormOptions] $FormOptions = @{}, [BrowserOptions] $BrowserOptions = @{}, [ScriptBlock] $OnShown = {}, [ScriptBlock] $OnComplete = {}, [ScriptBlock] $OnClose = {}, [Switch] $Default, [Switch] $PassThru ) Begin { Use-Web -Browser $Form = $FormOptions.SetProperties((New-Object 'Windows.Forms.Form')) $Browser = $BrowserOptions.SetProperties((New-Object 'Windows.Forms.WebBrowser')) $Browser.Size = @{ Width = $Form.Width; Height = $Form.Height } $Form.Controls.Add($Browser) $ShownCallback = { '==> Form shown' | Write-Verbose $Form.BringToFront() & $OnShown -Form $Form -Browser $Browser } $CompletedCallback = { "==> Document load complete ($($_.Url))" | Write-Verbose & $OnComplete -Form $Form -Browser $Browser } $Form.Add_Shown($ShownCallback); $Browser.Add_DocumentCompleted($CompletedCallback) } Process { "==> Browser is $(if($Browser.IsOffline) { 'OFFLINE' } else { 'ONLINE' })" | Write-Verbose $IsFile = if (Test-Path $Content -IsValid) { Test-Path $Content } else { $False } $IsUri = ([Uri]$Content).IsAbsoluteUri if ($Default) { $FilePath = if ($IsFile -or $IsUri) { $Content } else { $TempRoot = if ($IsLinux) { '/tmp' } else { $Env:temp } $Path = Join-Path $TempRoot 'content.html' $Content | Set-Content -Path $Path $Path } $Process = Start-Process -FilePath $FilePath -PassThru if ($PassThru) { return $Process } } else { if ($IsFile) { "==> Opening ${Content}..." | Write-Verbose $Browser.Navigate("file:///$((Get-Item $Content).Fullname)") } elseif ($IsUri) { "==> Navigating to ${Content}..." | Write-Verbose $Browser.Navigate([Uri]$Content) } else { '==> Opening HTML in WebBrowser control...' | Write-Verbose $Browser.DocumentText = "$Content" } if ($Form.ShowDialog() -ne 'OK') { $Document = $Browser.Document '==> Browser closing...' | Write-Verbose & $OnClose -Form $Form -Browser $Browser $Form.Dispose() '==> Form disposed' | Write-Verbose if ($PassThru) { return $Document } } } } } function Test-Url { <# .SYNOPSIS Test if a URL is accessible .PARAMETER Code Return status code as a string instead of boolean value .PARAMETER WebRequestParameters Object for passing parameters to underlying invocation of Invoke-WebRequest .EXAMPLE 'https://google.com' | Test-Url #> [CmdletBinding()] [OutputType([Bool])] [OutputType([String])] Param( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [UriBuilder] $Value, [Switch] $Code, [PSObject] $WebRequestParameters = @{} ) Process { $Response = try { Invoke-WebRequestBasicAuth -Uri $Value.Uri -WebRequestParameters $WebRequestParameters } catch { @{ StatusCode = '404' } } $StatusCode = $Response | Get-Property StatusCode switch ($StatusCode) { 200 { if ($Code) { '200' } else { $True } } Default { if ($Code) { $StatusCode.ToString() } else { $False } } } } } function Update-HostsFile { <# .SYNOPSIS Update and/or add entries of a hosts file. .PARAMETER Path Specifies an alternate hosts path. Defaults to %SystemRoot%\System32\drivers\etc\hosts. .PARAMETER PassThru Outputs parsed HOSTS file upon completion. .EXAMPLE Update-HostsFile -IPAddress '127.0.0.1' -Hostname 'c2.evil.com' .EXAMPLE Update-HostsFile -IPAddress '127.0.0.1' -Hostname 'c2.evil.com' -Comment 'Malware C2' #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory = $True, Position = 0)] [Alias('IP')] [Net.IpAddress] $IPAddress, [Parameter(Mandatory = $True, Position = 1)] [ValidateNotNullOrEmpty()] [Alias('Name')] [String] $Hostname, [Parameter(Position = 2)] [String] $Comment, [ValidateScript( { Test-Path $_ })] [String] $Path = (Join-Path $Env:SystemRoot 'System32\drivers\etc\hosts'), [Switch] $PassThru ) $Raw = Get-Content $Path $Hosts = Get-HostsContent $Path $Comment = if ($Comment) { "# $Comment" } else { '' } $Entry = "$IpAddress $Hostname $Comment" $HostExists = $Hostname -in $Hosts.Hostname $Hosts | Where-Object { $_.Hostname -eq $Hostname } | ForEach-Object { if ($_.IpAddress -eq $IPAddress) { "The hostname, '$Hostname', and IP address, '$IPAddress', already exist in $Path." | Write-Verbose } else { if ($PSCmdlet.ShouldProcess($Path)) { "Replacing hostname, '$Hostname', in $Path." | Write-Verbose $Raw[$_.LineNumber] = $Entry } else { "==> Would be replacing hostname, '$Hostname', in $Path." | Write-Color -DarkGray } } } if (-not $HostExists) { if ($PSCmdlet.ShouldProcess($Path)) { "Appending '$Hostname' at '$IPAddress' to $Path." | Write-Verbose $Raw += "`n$Entry" } else { "==> Would be appending '$Hostname' at '$IPAddress' to $Path." | Write-Color -DarkGray } } $Raw | Out-File -Encoding ascii -FilePath $Path -ErrorAction Stop if ($PassThru) { Get-HostsContent $Path } } function Use-Web { <# .SYNOPSIS Load related types for using web (with or without a web browser), if types are not already loaded. .PARAMETER Browser Whether or not to load WebBrowser type #> [CmdletBinding()] Param( [Switch] $Browser, [Switch] $PassThru ) if (-not ('System.Web.HttpUtility' -as [Type])) { '==> Adding System.Web types' | Write-Verbose Add-Type -AssemblyName System.Web } else { '==> System.Web is already loaded' | Write-Verbose } if ($Browser) { if (-not ('System.Windows.Forms.WebBrowser' -as [Type])) { '==> Adding System.Windows.Forms types' | Write-Verbose Add-Type -AssemblyName System.Windows.Forms } else { '==> System.Windows.Forms is already loaded' | Write-Verbose } } if ($PassThru) { $True } } |