2015-10-13 14:47:56 +00:00
#!powershell
2017-09-19 23:11:36 +00:00
# Copyright: (c) 2015, Corwin Brown <corwin@corwinbrown.com>
# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2017-10-23 18:40:54 +00:00
# Requires -Module Ansible.ModuleUtils.Legacy
2018-02-07 09:58:47 +00:00
# Requires -Module Ansible.ModuleUtils.CamelConversion
# Requires -Module Ansible.ModuleUtils.FileUtil
2015-10-13 14:47:56 +00:00
2017-01-27 23:24:32 +00:00
$ErrorActionPreference = " Stop "
2017-09-19 23:11:36 +00:00
$params = Parse-Args -arguments $args -supports_check_mode $true
2017-06-19 16:30:08 +00:00
$check_mode = Get-AnsibleParam -obj $params -name " _ansible_check_mode " -type " bool " -default $false
2017-01-27 23:24:32 +00:00
2017-02-24 07:35:52 +00:00
$url = Get-AnsibleParam -obj $params -name " url " -type " str " -failifempty $true
2018-02-07 09:58:47 +00:00
$method = Get-AnsibleParam -obj $params " method " -type " str " -default " GET " -validateset " CONNECT " , " DELETE " , " GET " , " HEAD " , " MERGE " , " OPTIONS " , " PATCH " , " POST " , " PUT " , " REFRESH " , " TRACE "
2017-02-24 07:35:52 +00:00
$content_type = Get-AnsibleParam -obj $params -name " content_type " -type " str "
2018-02-07 09:58:47 +00:00
$headers = Get-AnsibleParam -obj $params -name " headers "
$body = Get-AnsibleParam -obj $params -name " body "
2017-01-27 23:24:32 +00:00
$dest = Get-AnsibleParam -obj $params -name " dest " -type " path "
2017-06-19 16:30:08 +00:00
$user = Get-AnsibleParam -obj $params -name " user " -type " str "
$password = Get-AnsibleParam -obj $params -name " password " -type " str "
2018-02-07 09:58:47 +00:00
$force_basic_auth = Get-AnsibleParam -obj $params -name " force_basic_auth " -type " bool " -default $false
2017-06-19 16:30:08 +00:00
$creates = Get-AnsibleParam -obj $params -name " creates " -type " path "
$removes = Get-AnsibleParam -obj $params -name " removes " -type " path "
$follow_redirects = Get-AnsibleParam -obj $params -name " follow_redirects " -type " str " -default " safe " -validateset " all " , " none " , " safe "
2018-02-07 09:58:47 +00:00
$maximum_redirection = Get-AnsibleParam -obj $params -name " maximum_redirection " -type " int " -default 50
2017-06-19 16:30:08 +00:00
$return_content = Get-AnsibleParam -obj $params -name " return_content " -type " bool " -default $false
$status_code = Get-AnsibleParam -obj $params -name " status_code " -type " list " -default @ ( 200 )
$timeout = Get-AnsibleParam -obj $params -name " timeout " -type " int " -default 30
$validate_certs = Get-AnsibleParam -obj $params -name " validate_certs " -type " bool " -default $true
$client_cert = Get-AnsibleParam -obj $params -name " client_cert " -type " path "
2018-02-07 09:58:47 +00:00
$client_cert_password = Get-AnsibleParam -obj $params -name " client_cert_password " -type " str "
2017-06-19 16:30:08 +00:00
2018-08-28 22:10:46 +00:00
$JSON_CANDIDATES = @ ( 'text' , 'json' , 'javascript' )
2018-02-14 20:17:23 +00:00
$result = @ {
changed = $false
2018-08-31 20:20:56 +00:00
elapsed = 0
2018-02-14 20:17:23 +00:00
url = $url
}
2018-02-07 09:58:47 +00:00
if ( $creates -and ( Test-AnsiblePath -Path $creates ) ) {
2017-06-19 16:30:08 +00:00
$result . skipped = $true
2017-09-19 23:11:36 +00:00
Exit-Json -obj $result -message " The 'creates' file or directory ( $creates ) already exists. "
2017-06-19 16:30:08 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $removes -and -not ( Test-AnsiblePath -Path $removes ) ) {
2017-06-19 16:30:08 +00:00
$result . skipped = $true
2017-09-19 23:11:36 +00:00
Exit-Json -obj $result -message " The 'removes' file or directory ( $removes ) does not exist. "
2017-06-19 16:30:08 +00:00
}
2017-01-27 23:24:32 +00:00
2018-04-10 02:25:08 +00:00
if ( $status_code ) {
$status_code = foreach ( $code in $status_code ) {
try {
[ int ] $code
}
catch [ System.InvalidCastException ] {
Fail-Json -obj $result -message " Failed to convert ' $code ' to an integer. Status codes must be provided in numeric format. "
}
}
}
2018-07-23 21:02:05 +00:00
# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
$security_protocols = [ Net.ServicePointManager ] :: SecurityProtocol -bor [ Net.SecurityProtocolType ] :: SystemDefault
if ( [ Net.SecurityProtocolType ] . GetMember ( " Tls11 " ) . Count -gt 0 ) {
$security_protocols = $security_protocols -bor [ Net.SecurityProtocolType ] :: Tls11
}
if ( [ Net.SecurityProtocolType ] . GetMember ( " Tls12 " ) . Count -gt 0 ) {
$security_protocols = $security_protocols -bor [ Net.SecurityProtocolType ] :: Tls12
}
[ Net.ServicePointManager ] :: SecurityProtocol = $security_protocols
2018-02-07 09:58:47 +00:00
$client = [ System.Net.WebRequest ] :: Create ( $url )
$client . Method = $method
$client . Timeout = $timeout * 1000
2017-06-19 16:30:08 +00:00
# Disable redirection if requested
switch ( $follow_redirects ) {
" none " {
2018-02-07 09:58:47 +00:00
$client . AllowAutoRedirect = $false
2017-06-19 16:30:08 +00:00
}
" safe " {
2018-02-07 09:58:47 +00:00
if ( @ ( " GET " , " HEAD " ) -notcontains $method ) {
$client . AllowAutoRedirect = $false
} else {
$client . AllowAutoRedirect = $true
2017-06-19 16:30:08 +00:00
}
2017-01-27 23:24:32 +00:00
}
2018-02-07 09:58:47 +00:00
default {
$client . AllowAutoRedirect = $true
}
2017-01-27 23:24:32 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $maximum_redirection -eq 0 ) {
# 0 is not a valid option, need to disable redirection through AllowAutoRedirect
$client . AllowAutoRedirect = $false
} else {
$client . MaximumAutomaticRedirections = $maximum_redirection
2017-01-27 23:24:32 +00:00
}
2016-02-08 21:15:05 +00:00
2017-06-19 16:30:08 +00:00
if ( -not $validate_certs ) {
2018-02-07 09:58:47 +00:00
[ System.Net.ServicePointManager ] :: ServerCertificateValidationCallback = { $true }
2017-06-19 16:30:08 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $null -ne $content_type ) {
$client . ContentType = $content_type
2017-06-19 16:30:08 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $headers ) {
$req_headers = New-Object -TypeName System . Net . WebHeaderCollection
foreach ( $header in $headers . GetEnumerator ( ) ) {
# some headers need to be set on the property itself
switch ( $header . Name ) {
Accept { $client . Accept = $header . Value }
Connection { $client . Connection = $header . Value }
Content-Length { $client . ContentLength = $header . Value }
Content-Type { $client . ContentType = $header . Value }
Expect { $client . Expect = $header . Value }
Date { $client . Date = $header . Value }
Host { $client . Host = $header . Value }
If-Modified -Since { $client . IfModifiedSince = $header . Value }
Range { $client . AddRange ( $header . Value ) }
Referer { $client . Referer = $header . Value }
Transfer-Encoding {
$client . SendChunked = $true
$client . TransferEncoding = $header . Value
}
User-Agent { $client . UserAgent = $header . Value }
default { $req_headers . Add ( $header . Name , $header . Value ) }
}
}
2018-03-25 19:35:41 +00:00
$client . Headers . Add ( $req_headers )
2016-11-07 21:04:09 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $client_cert ) {
if ( -not ( Test-AnsiblePath -Path $client_cert ) ) {
Fail-Json -obj $result -message " Client certificate ' $client_cert ' does not exist "
}
try {
$certs = New-Object -TypeName System . Security . Cryptography . X509Certificates . X509Certificate2Collection -ArgumentList $client_cert , $client_cert_password
$client . ClientCertificates = $certs
} catch [ System.Security.Cryptography.CryptographicException ] {
Fail-Json -obj $result -message " Failed to read client certificate ' $client_cert ': $( $_ . Exception . Message ) "
} catch {
Fail-Json -obj $result -message " Unhandled exception when reading client certificate at ' $client_cert ': $( $_ . Exception . Message ) "
}
2017-01-04 21:33:47 +00:00
}
2017-06-19 16:30:08 +00:00
if ( $user -and $password ) {
2018-02-07 09:58:47 +00:00
if ( $force_basic_auth ) {
$basic_value = [ Convert ] :: ToBase64String ( [ System.Text.Encoding ] :: ASCII . GetBytes ( " $( $user ) : $( $password ) " ) )
$client . Headers . Add ( " Authorization " , " Basic $basic_value " )
} else {
$sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force
$credential = New-Object -TypeName System . Management . Automation . PSCredential -ArgumentList $user , $sec_password
$client . Credentials = $credential
}
2017-10-30 00:43:43 +00:00
} elseif ( $user -or $password ) {
2017-09-19 23:11:36 +00:00
Add-Warning -obj $result -message " Both 'user' and 'password' parameters are required together, skipping authentication "
2017-01-27 23:24:32 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $null -ne $body ) {
if ( $body -is [ Hashtable ] ) {
$body_string = ConvertTo-Json -InputObject $body -Compress
} elseif ( $body -isnot [ String ] ) {
$body_string = $body . ToString ( )
} else {
$body_string = $body
}
$buffer = [ System.Text.Encoding ] :: UTF8 . GetBytes ( $body_string )
$req_st = $client . GetRequestStream ( )
try {
$req_st . Write ( $buffer , 0 , $buffer . Length )
} finally {
$req_st . Flush ( )
$req_st . Close ( )
}
2017-06-19 16:30:08 +00:00
}
2018-08-31 20:20:56 +00:00
$module_start = Get-Date
2018-02-07 09:58:47 +00:00
try {
$response = $client . GetResponse ( )
2018-03-07 23:43:42 +00:00
} catch [ System.Net.WebException ] {
2018-08-31 20:20:56 +00:00
$result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
2018-03-07 23:43:42 +00:00
$response = $null
if ( $_ . Exception . PSObject . Properties . Name -match " Response " ) {
# was a non-successful response but we at least have a response and
# should parse it below according to module input
$response = $_ . Exception . Response
}
# in the case a response (or empty response) was on the exception like in
# a timeout scenario, we should still fail
if ( $null -eq $response ) {
2018-08-31 20:20:56 +00:00
$result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
2018-03-07 23:43:42 +00:00
Fail-Json -obj $result -message " WebException occurred when sending web request: $( $_ . Exception . Message ) "
}
2018-02-07 09:58:47 +00:00
} catch [ System.Net.ProtocolViolationException ] {
2018-08-31 20:20:56 +00:00
$result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
2018-02-07 09:58:47 +00:00
Fail-Json -obj $result -message " ProtocolViolationException when sending web request: $( $_ . Exception . Message ) "
} catch {
2018-08-31 20:20:56 +00:00
$result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
2018-02-07 09:58:47 +00:00
Fail-Json -obj $result -message " Unhandled exception occured when sending web request. Exception: $( $_ . Exception . Message ) "
2015-10-13 14:47:56 +00:00
}
2018-08-31 20:20:56 +00:00
$result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
2015-10-13 14:47:56 +00:00
ForEach ( $prop in $response . psobject . properties ) {
2018-02-07 09:58:47 +00:00
$result_key = Convert-StringToSnakeCase -string $prop . Name
2018-03-07 23:43:42 +00:00
$prop_value = $prop . Value
# convert and DateTime values to ISO 8601 standard
if ( $prop_value -is [ System.DateTime ] ) {
$prop_value = $prop_value . ToString ( " o " , [ System.Globalization.CultureInfo ] :: InvariantCulture )
}
$result . $result_key = $prop_value
2015-10-13 14:47:56 +00:00
}
2018-02-07 09:58:47 +00:00
# manually get the headers as not all of them are in the response properties
foreach ( $header_key in $response . Headers . GetEnumerator ( ) ) {
$header_value = $response . Headers [ $header_key ]
$header_key = $header_key . Replace ( " - " , " " ) # replace - with _ for snake conversion
$header_key = Convert-StringToSnakeCase -string $header_key
$result . $header_key = $header_value
}
# we only care about the return body if we need to return the content or create a file
if ( $return_content -or $dest ) {
$resp_st = $response . GetResponseStream ( )
# copy to a MemoryStream so we can read it multiple times
$memory_st = New-Object -TypeName System . IO . MemoryStream
try {
$resp_st . CopyTo ( $memory_st )
$resp_st . Close ( )
if ( $return_content ) {
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin )
$content_bytes = $memory_st . ToArray ( )
$result . content = [ System.Text.Encoding ] :: UTF8 . GetString ( $content_bytes )
2018-08-28 22:10:46 +00:00
if ( $result . ContainsKey ( " content_type " ) -and $result . content_type -Match ( $JSON_CANDIDATES -join '|' ) ) {
try {
$result . json = ConvertFrom-Json -InputObject $result . content
} catch [ System.ArgumentException ] {
# Simply continue, since 'text' might be anything
}
2018-02-07 09:58:47 +00:00
}
}
if ( $dest ) {
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin )
$changed = $true
if ( Test-AnsiblePath -Path $dest ) {
$actual_checksum = Get-FileChecksum -path $dest -algorithm " sha1 "
$sp = New-Object -TypeName System . Security . Cryptography . SHA1CryptoServiceProvider
$content_checksum = [ System.BitConverter ] :: ToString ( $sp . ComputeHash ( $memory_st ) ) . Replace ( " - " , " " ) . ToLower ( )
2018-08-28 22:10:46 +00:00
2018-02-07 09:58:47 +00:00
if ( $actual_checksum -eq $content_checksum ) {
$changed = $false
}
}
$result . changed = $changed
if ( $changed -and ( -not $check_mode ) ) {
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin )
$file_stream = [ System.IO.File ] :: Create ( $dest )
try {
$memory_st . CopyTo ( $file_stream )
} finally {
$file_stream . Flush ( )
$file_stream . Close ( )
}
}
}
} finally {
$memory_st . Close ( )
}
}
2018-03-07 23:43:42 +00:00
if ( $status_code -notcontains $response . StatusCode ) {
2018-04-10 02:25:08 +00:00
Fail-Json -obj $result -message " Status code of request ' $( [ int ] $response . StatusCode ) ' is not in list of valid status codes $status_code : ' $( $response . StatusCode ) '. "
2018-03-07 23:43:42 +00:00
}
2018-02-07 09:58:47 +00:00
2018-03-07 23:43:42 +00:00
Exit-Json -obj $result