Skip to content

Commit

Permalink
Merge pull request #13 from Badgerati/2fa-attrs
Browse files Browse the repository at this point in the history
Adds support for 2FA, attributes, running JavaScript, and fix clicking elements
  • Loading branch information
Badgerati committed Oct 16, 2019
2 parents 2df9375 + 7a39b9b commit a76f764
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 461 deletions.
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Monocle is a Cross-Platform PowerShell Web Automation module, made to make autom
* [Documentation](#documentation)
* [Functions](#functions)
* [Screenshots](#screenshots)
* [Waiting](#waiting)
* [2FA Codes](#2fa-codes)
* [Docker](#docker)

Monocle currently supports the following browsers:
Expand Down Expand Up @@ -41,19 +41,19 @@ Start-MonocleFlow -Name 'Load YouTube' -Browser $browser -ScriptBlock {
Set-MonocleUrl -Url 'https://www.youtube.com'
# sets the element's value, selecting the element by ID/Name
Set-MonocleElementValue -Id 'search_query' -Value 'Beerus Madness (Extended)'
Get-MonocleElement -Id 'search_query' | Set-MonocleElementValue -Value 'Beerus Madness (Extended)'
# click the search button
Invoke-MonocleElementClick -Id 'search-btn'
Get-MonocleElement -Id 'search-icon-legacy' | Invoke-MonocleElementClick
# wait for the URL to change to start with the following value
Wait-MonocleUrl -Url 'https://www.youtube.com/results?search_query=' -StartsWith
# downloads an image from the page, selcted by using an XPath to an element
Save-MonocleImage -XPath "//div[@data-context-item-id='SI6Yyr-iI6M']/img[1]" -Path '.\beerus.jpg'
Get-MonocleElement -XPath "//div[@data-context-item-id='SI6Yyr-iI6M']/img[1]" | Save-MonocleImage -FilePath '.\beerus.jpg'
# tells the browser to click the video in the results
Invoke-MonocleElementClick -XPath "//a[@title='Dragon Ball Super Soundtrack - Beerus Madness (Extended)']"
Get-MonocleElement -XPath "//a[@title='Dragon Ball Super Soundtrack - Beerus Madness (Extended)']" | Invoke-MonocleElementClick
# wait for the URL to be loaded
Wait-MonocleUrl -Url 'https://www.youtube.com/watch?v=SI6Yyr-iI6M'
Expand All @@ -72,25 +72,33 @@ The following is a list of available functions in Monocle:

* Assert-MonocleBodyValue
* Assert-MonocleElementValue
* Clear-MonocleElementValue
* Close-MonocleBrowser
* Edit-MonocleUrl
* Get-Monocle2FACode
* Get-MonocleElement
* Get-MonocleElementAttribute
* Get-MonocleElementValue
* Get-MonocleHtml
* Get-MonocleTimeout
* Get-MonocleUrl
* Invoke-MonocleElementCheck
* Invoke-MonocleElementClick
* Invoke-MonocleJavaScript
* Invoke-MonocleRetryScript
* Invoke-MonocleScreenshot
* New-MonocleBrowser
* Restart-MonocleBrowser
* Save-MonocleImage
* Set-MonocleElementAttribute
* Set-MonocleElementValue
* Set-MonocleTimeout
* Set-MonocleUrl
* Start-MonocleFlow
* Start-MonocleSleep
* Submit-MonocleForm
* Test-MonocleElement
* Wait-MonocleElement
* Test-MonocleElementAttribute
* Wait-MonocleUrl
* Wait-MonocleUrlDifferent
* Wait-MonocleValue
Expand All @@ -113,17 +121,18 @@ Invoke-MonocoleScreenshot -Name 'screenshot.png' -Path './path'

> Not supplying `-ScreenshotPath` or `-Path` will default to the current path.
### Waiting
### 2FA Codes

There are inbuilt function to wait for a URL or element. However, to wait for an element during a Set/Click call you can use the `-Wait` switch:
Monocle has inbuilt support for generating 2FA codes. To do this you need the Secret Code that is normally presented with the QR code, and you pass this to the `Get-Monocle2FACode` function with a date - which is defaulted to now:

```powershell
Invoke-MonocleElementClick -Id 'element-id' -Wait
$code = Get-Monocle2FACode -Secret 'FAKENDMYJWLLB'
Get-MonocleElement -Id '2fa-code' | Set-MonocleElementValue -Value $code -Mask
```

### Docker

Monocle has an offical Docker image, which comes preloaded with:
Monocle has an official Docker image, which comes preloaded with:

* Monocle (obviously!)
* Firefox
Expand Down
8 changes: 4 additions & 4 deletions examples/youtube.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ Start-MonocleFlow -Name 'Load YouTube' -Browser $browser -ScriptBlock {
Set-MonocleUrl -Url 'https://www.youtube.com'

# Sets the search bar element to the passed value to query
Set-MonocleElementValue -Id 'search_query' -Value 'Beerus Madness (Extended)'
Get-MonocleElement -Id 'search_query' | Set-MonocleElementValue -Value 'Beerus Madness (Extended)'

# Tells the browser to click the search button
Invoke-MonocleElementClick -Id 'search-icon-legacy'
Get-MonocleElement -Id 'search-icon-legacy' | Invoke-MonocleElementClick

# Though all commands sleep when the page is busy, some buttons use javascript
# to reform the page. The following will sleep the browser until the passed URL is loaded.
# If (default) 10 seconds passes and no URL, then the flow fails
Wait-MonocleUrl -Url 'https://www.youtube.com/results?search_query=' -StartsWith

# Downloads an image from the page. This time it's using XPath
#Save-MonocleImage -XPath "//div[@data-context-item-id='SI6Yyr-iI6M']/img[1]" -Path '.\beerus.jpg'
#Get-MonocleElement -XPath "//div[@data-context-item-id='SI6Yyr-iI6M']/img[1]" | Save-MonocleImage -FilePath '.\beerus.jpg'

# Tells the browser to click the video in the results. The video link is found via XPath
Invoke-MonocleElementClick -XPath "//a[@title='Dragon Ball Super Soundtrack - Beerus Madness (Extended)']" -Wait
Get-MonocleElement -XPath "//a[@title='Dragon Ball Super Soundtrack - Beerus Madness (Extended)']" | Invoke-MonocleElementClick

# Again, we expect the URL to be loaded
Wait-MonocleUrl -Url 'https://www.youtube.com/watch?v=SI6Yyr-iI6M'
Expand Down
4 changes: 2 additions & 2 deletions src/Monocle.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
RootModule = 'Monocle.psm1'

# Version number of this module.
ModuleVersion = '1.0.0'
ModuleVersion = '1.1.0'

# ID used to uniquely identify this module
GUID = '9dc3c8a1-664d-4253-a5d2-920250d3a15f'
Expand All @@ -33,7 +33,7 @@
PSData = @{

# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('powershell', 'web', 'automation', 'testing', 'ie', 'internet-explorer', 'websites', 'chrome',
Tags = @('powershell', 'web', 'automation', 'testing', 'ie', 'internet-explorer', 'websites', 'chrome', '2fa',
'firefox', 'selenium', 'cross-platform', 'PSEdition_Core', 'PSEdition_Desktop', 'linux', 'google-chrome')

# A URL to the license for this module.
Expand Down
52 changes: 34 additions & 18 deletions src/Private/Elements.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
function Get-MonocleElement
function Get-MonocleElementId
{
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[OpenQA.Selenium.IWebElement]
$Element
)

return (Get-MonocleElementAttribute -Element $Element -Name 'meta-monocle-id')
}

function Set-MonocleElementId
{
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[OpenQA.Selenium.IWebElement]
$Element,

[Parameter(Mandatory=$true)]
[string]
$Id
)

return (Set-MonocleElementAttribute -Element $Element -Name 'meta-monocle-id' -Value $Id)
}

function Get-MonocleElementInternal
{
[CmdletBinding()]
param (
Expand Down Expand Up @@ -31,21 +59,11 @@ function Get-MonocleElement
[string]
$XPath,

[Parameter()]
[int]
$Timeout,

[switch]
$NoThrow,

[switch]
$Wait
$NoThrow
)

if ($Timeout -le 0) {
$Timeout = 10
}

$timeout = Get-MonocleTimeout
$seconds = 0

while ($true) {
Expand All @@ -72,7 +90,7 @@ function Get-MonocleElement
catch {
$seconds++

if (!$Wait -or ($seconds -ge $Timeout)) {
if ($seconds -ge $timeout) {
throw $_.Exception
}

Expand Down Expand Up @@ -109,7 +127,7 @@ function Get-MonocleElementById

return @{
Element = $element
Id = "<$($Id)>"
Id = "<[@id=$($Id)]>"
}
}

Expand Down Expand Up @@ -137,8 +155,6 @@ function Get-MonocleElementByTagName
$NoThrow
)

#$document = $Browser.Document

# get all elements for the tag
Write-Verbose -Message "Finding element with tag <$TagName>"
$elements = $Browser.FindElementsByTagName($TagName)
Expand Down Expand Up @@ -170,7 +186,7 @@ function Get-MonocleElementByTagName
throw "Element <$TagName> with attribute '$AttributeName' and value of '$AttributeValue' not found"
}

$id += "[$($AttributeName)=$($AttributeValue)]"
$id += "[@$($AttributeName)=$($AttributeValue)]"
}

if (![string]::IsNullOrWhiteSpace($ElementValue))
Expand Down
29 changes: 13 additions & 16 deletions src/Private/Tools.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ function Start-MonocleSleepWhileBusy
param ()

$count = 0
$timeout = $Browser.Manage().Timeouts().PageLoad
$timeout = Get-MonocleTimeout

while ($Browser.ExecuteScript('return document.readyState') -ine 'complete')
while ((Invoke-MonocleJavaScript -Script 'return document.readyState') -ine 'complete')
{
if ($count -ge $timeout) {
throw "Loading URL has timed-out after $timeout second(s)"
Expand Down Expand Up @@ -82,30 +82,27 @@ function Test-MonocleUrl
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[string]
$Url,

[Parameter()]
[int]
$Attempts = 1
$Url
)

# truncate the URL of any query parameters
$Url = ([System.Uri]$Url).GetLeftPart([System.UriPartial]::Path)

# initial code setting as success
$code = 200
$timeout = Get-MonocleTimeout
$message = [string]::Empty

$attempt = 1
while ($attempt -le $Attempts) {
$count = 1
while ($count -le $timeout) {
try {
Write-MonocleHost -Message "Testing: $url [attempt: $($attempt)]"
Write-MonocleHost -Message "Testing: $url [attempt: $($count)]"

if ($PSVersionTable.PSVersion.Major -le 5) {
$result = Invoke-WebRequest -Uri $Url -TimeoutSec 30 -UseBasicParsing -ErrorAction Stop
$result = Invoke-WebRequest -Uri $Url -TimeoutSec $timeout -UseBasicParsing -ErrorAction Stop
}
else {
$result = Invoke-WebRequest -Uri $Url -TimeoutSec 30 -ErrorAction Stop
$result = Invoke-WebRequest -Uri $Url -TimeoutSec $timeout -ErrorAction Stop
}

$code = [int]$result.StatusCode
Expand All @@ -131,12 +128,12 @@ function Test-MonocleUrl
}

if (($code -eq -1) -or ($code -ge 400)) {
$attempt++
Start-Sleep -Seconds 1

if ($attempt -gt $Attempts) {
$count++
if ($count -gt $timeout) {
break
}

Start-Sleep -Seconds 1
}
else {
break
Expand Down
100 changes: 100 additions & 0 deletions src/Private/TwoFactor.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
function Get-Monocle2FAInterval
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[DateTime]
$DateTime
)

# convert to utc
$DateTime = $DateTime.ToUniversalTime()

# get time interval for the date
$secondsPerInterval = 30
$epochTime = Get-Date "01/01/1970 00:00:00"
$secondsSinceEpochTime = (New-TimeSpan -Start $epochTime -End $DateTime).TotalSeconds

return [int64][math]::Floor($secondsSinceEpochTime / $secondsPerInterval)
}

function Get-Monocle2FAPin
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]
$Secret,

[Parameter(Mandatory=$true)]
[long]
$Interval
)

# convert the parameters to bytes
$secretAsBytes = Convert-Monocle2FASecretToBytes -Secret $Secret
$timeBytes = Convert-Monocle2FAIntervalToBytes -Interval $Interval

# do the HMAC calculation with the default SHA1
$hmacGen = [Security.Cryptography.HMACSHA1]::new($secretAsBytes)
$hash = $hmacGen.ComputeHash($timeBytes)

# take half the last byte
$offset = ($hash[$hash.Length - 1] -band 0xF)

# use it as an index into the hash bytes and take 4 bytes from there, big-endian needed
$fourBytes = $hash[$offset..($offset + 3)]
if ([BitConverter]::IsLittleEndian) {
[array]::Reverse($fourBytes)
}

# remove the most significant bit
$num = ([BitConverter]::ToInt32($fourBytes, 0) -band 0x7FFFFFFF)
return ($num % 1000000).ToString().PadLeft(6, '0')
}

function Convert-Monocle2FASecretToBytes
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]
$Secret
)

$Base32Charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

# convert the secret from BASE32 to a byte array via a BigInteger so we can use its bit-shifting support
$bigInteger = [Numerics.BigInteger]::Zero
foreach ($char in ($secret.ToUpper() -replace '[^A-Z2-7]').GetEnumerator()) {
$bigInteger = (($bigInteger -shl 5) -bor ($Base32Charset.IndexOf($char)))
}

[byte[]]$secretAsBytes = $bigInteger.ToByteArray()

# BigInteger sometimes adds a 0 byte to the end, if it happens, we need to remove it
if ($secretAsBytes[-1] -eq 0) {
$secretAsBytes = $secretAsBytes[0..($secretAsBytes.Count - 2)]
}

# BigInteger stores bytes in Little-Endian order, but we need them in Big-Endian order.
[array]::Reverse($secretAsBytes)
return $secretAsBytes
}

function Convert-Monocle2FAIntervalToBytes
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[long]
$Interval
)

$timeBytes = [BitConverter]::GetBytes($Interval)
if ([BitConverter]::IsLittleEndian) {
[array]::Reverse($timeBytes)
}

return $timeBytes
}
Loading

0 comments on commit a76f764

Please sign in to comment.