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)
2018-11-07 00:53:17 +00:00
#AnsibleRequires -CSharpUtil Ansible.Basic
2018-02-07 09:58:47 +00:00
# Requires -Module Ansible.ModuleUtils.CamelConversion
# Requires -Module Ansible.ModuleUtils.FileUtil
2018-11-07 00:53:17 +00:00
# Requires -Module Ansible.ModuleUtils.Legacy
2015-10-13 14:47:56 +00:00
2018-11-07 00:53:17 +00:00
$spec = @ {
options = @ {
url = @ { type = " str " ; required = $true }
method = @ {
type = " str "
default = " GET "
choices = " CONNECT " , " DELETE " , " GET " , " HEAD " , " MERGE " , " OPTIONS " , " PATCH " , " POST " , " PUT " , " REFRESH " , " TRACE "
}
content_type = @ { type = " str " }
headers = @ { type = " dict " }
body = @ { type = " raw " }
dest = @ { type = " path " }
user = @ { type = " str " }
password = @ { type = " str " ; no_log = $true }
force_basic_auth = @ { type = " bool " ; default = $false }
creates = @ { type = " path " }
removes = @ { type = " path " }
follow_redirects = @ {
type = " str "
default = " safe "
choices = " all " , " none " , " safe "
}
maximum_redirection = @ { type = " int " ; default = 50 }
return_content = @ { type = " bool " ; default = $false }
status_code = @ { type = " list " ; elements = " int " ; default = @ ( 200 ) }
timeout = @ { type = " int " ; default = 30 }
validate_certs = @ { type = " bool " ; default = $true }
client_cert = @ { type = " path " }
client_cert_password = @ { type = " str " ; no_log = $true }
}
supports_check_mode = $true
}
$module = [ Ansible.Basic.AnsibleModule ] :: Create ( $args , $spec )
$url = $module . Params . url
$method = $module . Params . method
$content_type = $module . Params . content_type
$headers = $module . Params . headers
$body = $module . Params . body
$dest = $module . Params . dest
$user = $module . Params . user
$password = $module . Params . password
$force_basic_auth = $module . Params . force_basic_auth
$creates = $module . Params . creates
$removes = $module . Params . removes
$follow_redirects = $module . Params . follow_redirects
$maximum_redirection = $module . Params . maximum_redirection
$return_content = $module . Params . return_content
$status_code = $module . Params . status_code
$timeout = $module . Params . timeout
$validate_certs = $module . Params . validate_certs
$client_cert = $module . Params . client_cert
$client_cert_password = $module . Params . client_cert_password
2017-06-19 16:30:08 +00:00
2018-08-28 22:10:46 +00:00
$JSON_CANDIDATES = @ ( 'text' , 'json' , 'javascript' )
2018-11-07 00:53:17 +00:00
$module . Result . elapsed = 0
$module . Result . url = $url
2018-02-14 20:17:23 +00:00
2018-02-07 09:58:47 +00:00
if ( $creates -and ( Test-AnsiblePath -Path $creates ) ) {
2018-11-07 00:53:17 +00:00
$module . Result . skipped = $true
$module . Result . msg = " The 'creates' file or directory ( $creates ) already exists. "
$module . ExitJson ( )
2017-06-19 16:30:08 +00:00
}
2018-02-07 09:58:47 +00:00
if ( $removes -and -not ( Test-AnsiblePath -Path $removes ) ) {
2018-11-07 00:53:17 +00:00
$module . Result . skipped = $true
$module . Result . msg = " The 'removes' file or directory ( $removes ) does not exist. "
$module . ExitJson ( )
2018-04-10 02:25:08 +00:00
}
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
2018-11-07 00:53:17 +00:00
switch ( $header . Key ) {
2018-02-07 09:58:47 +00:00
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 }
2018-11-07 00:53:17 +00:00
default { $req_headers . Add ( $header . Key , $header . Value ) }
2018-02-07 09:58:47 +00:00
}
}
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 ) ) {
2018-11-07 00:53:17 +00:00
$module . FailJson ( " Client certificate ' $client_cert ' does not exit " )
2018-02-07 09:58:47 +00:00
}
try {
$certs = New-Object -TypeName System . Security . Cryptography . X509Certificates . X509Certificate2Collection -ArgumentList $client_cert , $client_cert_password
$client . ClientCertificates = $certs
} catch [ System.Security.Cryptography.CryptographicException ] {
2018-11-07 00:53:17 +00:00
$module . FailJson ( " Failed to read client certificate ' $client_cert ': $( $_ . Exception . Message ) " , $_ )
2018-02-07 09:58:47 +00:00
} catch {
2018-11-07 00:53:17 +00:00
$module . FailJson ( " Unhandled exception when reading client certificate at ' $client_cert ': $( $_ . Exception . Message ) " , $_ )
2018-02-07 09:58:47 +00:00
}
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 ) {
2018-11-07 00:53:17 +00:00
$module . Warn ( " 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 ) {
2018-12-12 04:06:54 +00:00
if ( $body -is [ System.Collections.IDictionary ] -or $body -is [ System.Collections.IList ] ) {
2018-02-07 09:58:47 +00:00
$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-11-07 00:53:17 +00:00
$module . 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-11-07 00:53:17 +00:00
$module . Result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
$module . FailJson ( " WebException occurred when sending web request: $( $_ . Exception . Message ) " , $_ )
2018-03-07 23:43:42 +00:00
}
2018-02-07 09:58:47 +00:00
} catch [ System.Net.ProtocolViolationException ] {
2018-11-07 00:53:17 +00:00
$module . Result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
$module . FailJson ( " ProtocolViolationException when sending web request: $( $_ . Exception . Message ) " , $_ )
2018-02-07 09:58:47 +00:00
} catch {
2018-11-07 00:53:17 +00:00
$module . Result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
$module . FailJson ( " Unhandled exception occured when sending web request. Exception: $( $_ . Exception . Message ) " , $_ )
2015-10-13 14:47:56 +00:00
}
2018-11-07 00:53:17 +00:00
$module . 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 )
}
2018-11-07 00:53:17 +00:00
$module . 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
2018-11-07 00:53:17 +00:00
$module . Result . $header_key = $header_value
2018-02-07 09:58:47 +00:00
}
# 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 ) {
2018-11-06 01:10:03 +00:00
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin ) > $null
2018-02-07 09:58:47 +00:00
$content_bytes = $memory_st . ToArray ( )
2018-11-07 00:53:17 +00:00
$module . Result . content = [ System.Text.Encoding ] :: UTF8 . GetString ( $content_bytes )
if ( $module . Result . ContainsKey ( " content_type " ) -and $module . Result . content_type -Match ( $JSON_CANDIDATES -join '|' ) ) {
2018-08-28 22:10:46 +00:00
try {
2018-11-07 00:53:17 +00:00
$module . Result . json = ( [ Ansible.Basic.AnsibleModule ] :: FromJson ( $module . Result . content ) )
2018-08-28 22:10:46 +00:00
} catch [ System.ArgumentException ] {
# Simply continue, since 'text' might be anything
}
2018-02-07 09:58:47 +00:00
}
}
if ( $dest ) {
2018-11-06 01:10:03 +00:00
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin ) > $null
2018-02-07 09:58:47 +00:00
$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
}
}
2018-11-07 00:53:17 +00:00
$module . Result . changed = $changed
if ( $changed -and ( -not $module . CheckMode ) ) {
2018-11-06 01:10:03 +00:00
$memory_st . Seek ( 0 , [ System.IO.SeekOrigin ] :: Begin ) > $null
2018-02-07 09:58:47 +00:00
$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-11-07 00:53:17 +00:00
$module . FailJson ( " 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-11-07 00:53:17 +00:00
$module . ExitJson ( )