Welcome PowerShell User! This recipe is just one of the hundreds of useful resources contained in the PowerShell Cookbook.
If you own the book already, login here to get free, online, searchable access to the entire book's content.
If not, the Windows PowerShell Cookbook is available at Amazon, or any of your other favourite book retailers. If you want to see what the PowerShell Cookbook has to offer, enjoy this free 90 page e-book sample: "The Windows PowerShell Interactive Shell".
You want to interact with a website or application that requires dynamic cookies, logins, or multiple requests.
Use the Invoke-WebRequest
cmdlet to download a web page, and access the
-SessionVariable
and -WebSession
parameters. For example, to retrieve the number of active Facebook notifications:
$cred
=
Get-Credential
$login
=
Invoke-WebRequest
http
:
//
www
.
.
com
/
login
.
php
-SessionVariable
fb
$login
.
Forms
[
0
].
Fields
.
=
$cred
.
GetNetworkCredential
().
UserName
$login
.
Forms
[
0
].
Fields
.
pass
=
$cred
.
GetNetworkCredential
().
Password
$main
=
Invoke-WebRequest
$login
.
Forms
[
0
].
Action
-WebSession
$fb
-Body
$login
-Method
Post
$main
.
ParsedHtml
.
getElementById
(
"notificationsCountValue"
).
InnerText
While many pages on the internet provide their information directly when you access a web page, many others aren’t so simple. For example, the site may be protected by a login page (which then sets cookies), followed by another form (which requires those cookies) that returns a search result.
Automating these scenarios almost always requires a fairly in-depth understanding of the web application in question, as well as how web applications work in general.
Even with that understanding, automating these scenarios usually requires a vast amount of scripting: parsing HTTP headers, sending them in subsequent requests, hand-crafting form POST responses, and more.
As an example of bare scripting of a Facebook login, consider the following example that merely determines the login cookie to be used in further page requests:
$Credential = Get-Credential ## Get initial cookies $wc = New-Object System.Net.WebClient $wc.Headers.Add("User-Agent", "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0;)") $result = $wc.DownloadString("https://www.facebook.com/") $cookie = $wc.ResponseHeaders["Set-Cookie"] $cookie = ($cookie.Split(',') -match '^\S+=\S+;' -replace ';.*','') -join '; ' $wc = New-Object System.Net.WebClient $wc.Headers.Add("User-Agent", "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0;)") $wc.Headers.Add("Cookie", $cookie) $postValues = New-Object System.Collections.Specialized.NameValueCollection $postValues.Add("email", $credential.GetNetworkCredential().Username) $postValues.Add("pass", $credential.GetNetworkCredential().Password) ## Get the resulting cookie, and convert it into the form to be returned ## in the query string $result = $wc.UploadValues( "https://login.facebook.com/login.php?login_attempt=1", $postValues) $cookie = $wc.ResponseHeaders["Set-Cookie"] $cookie = ($cookie.Split(',') -match '^\S+=\S+;' -replace ';.*','') -join '; ' $cookie
This is just for the login. Scripting a full web session using this manual approach can easily take hundreds of lines of script.
If supported in your version of PowerShell, the -SessionVariable
and -WebSession
parameters of the Invoke-WebRequest
cmdlet don’t remove the need to understand how your target web application works. They do, however, remove the drudgery and complexity of dealing with the bare HTTP requests and responses. This improved session support comes primarily through four features:
Most web applications store their state in cookies—session IDs and login information being the two most common things to store. When a web application requests that a cookie be stored or deleted, Invoke-WebRequest
automatically records this information in the provided session variable. Subsequent requests that use this session variable automatically supply any cookies required by the web application. You can see the cookies in use by looking at the Cookies
property of the session variable:
$fb
.
Cookies
.
GetCookies
(
"https://www.facebook.com"
)
|
Select
Name
,
Value
After you submit a web form (especially a login form), many sites redirect through a series of intermediate pages before you finally land on the destination page. In basic HTTP scripting, this forces you to handle the many HTTP redirect status codes, parse the Location
header, and resubmit all the appropriate values. The Invoke-WebRequest
cmdlet handles this for you; the result it returns comes from the final page in any redirect sequences. If you wish to override this behavior, use the -MaximumRedirection
parameter.
Applications that require advanced session scripting tend to take most of their input data from fields in HTML forms, rather than items in the URL itself. Invoke-WebRequest
exposes these forms through the Forms
property of its result. This collection returns the form ID (useful if there are multiple forms), the form action (URL that should be used to submit the form), and fields defined by the form.
In traditional HTTP scripting, submitting a form is a complicated process. You need to gather all the form fields, encode them properly, determine the resulting encoded length, and POST all of this data to the destination URL.
Invoke-WebRequest
makes this very simple through the -Body
parameter used as input when you select POST
as the value of the -Method
parameter. The -Body
parameter accepts input in one of three formats:
The result of a previous Invoke-WebRequest
call, in which case values from the first form are used (if the response contains only one form).
A specific form (as manually selected from the Forms
property of a previous Invoke-WebRequest
call), in which case values from that form are used.
An IDictionary
(hashtable), in which case names and values from that dictionary are used.
An XML node, in which case the XML is encoded directly. This is used primarily for scripting REST APIs, and is unlikely to be used when scripting web application sessions.
A byte array, in which case the bytes are used and encoded directly. This is used primarily for scripting data uploads.
Let’s take a look at how these play a part in the script from the Solution, which detects how many notifications are pending on Facebook. Given how fast web applications change, it’s unlikely that this example will continue to work for long. It does demonstrate the thought process, however.
When you first connect to Facebook, you need to log in. Facebook funnels this through a page called login.php:
$login
=
Invoke-WebRequest
http
:
//
www
.
.
com
/
login
.
php
-SessionVariable
fb
If you look at the page that gets returned, there is a single form that includes email
and pass
fields:
PS > $login.Forms.Fields Key Value --- ----- (...) return_session 0 legacy_return 1 session_key_only 0 trynum 1 email pass persist_box 1 default_persistent 0 (...)
We fill these in:
$cred
=
Get-Credential
$login
.
Forms
[
0
].
Fields
.
=
$cred
.
UserName
$login
.
Forms
[
0
].
Fields
.
pass
=
$cred
.
GetNetworkCredential
().
Password
And submit the form. We use $fb
for the -WebSession
parameter, as that is what we used during the original request. We POST
to the URL referred to in the Action
field of the login form, and use the $login
variable as the request body. The $login
variable is the response that we got from the first request, where we customized the email
and pass
form fields. PowerShell recognizes that this was the result of a previous web request, and uses that single form as the POST
body:
$mainPage
=
Invoke-WebRequest
$login
.
Forms
[
0
].
Action
-WebSession
$fb
`
-Body
$login
-Method
Post
If you look at the raw HTML returned by this response (the Content
property), you can see that the notification count is contained in a span
element with the ID of
notificationsCountValue
:
(...) <span id="notificationsCountValue">1</span> (...)
To retrieve this element, we use the ParsedHtml
property of the response, call the GetElementById
method, and return the InnerText
property:
$mainPage
.
ParsedHtml
.
getElementById
(
"notificationsCountValue"
).
InnerText
Using these techniques, we can unlock a great deal of functionality on the internet previously hidden behind complicated HTTP scripting.
For more information about using the ParsedHtml
property to parse and analyze web pages, see Recipe 12.5.
Recipe 12.5, “Parse and Analyze a Web Page from the Internet”