2014-09-26 01:01:01 +00:00
#!powershell
2018-07-17 21:29:05 +00:00
# Copyright: (c) 2015, Paul Durivage <paul.durivage@rackspace.com>
# Copyright: (c) 2015, Tal Auslander <tal@cloudshare.com>
2017-08-29 00:11:10 +00:00
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
2019-03-05 10:37:00 +00:00
# Copyright: (c) 2019, Viktor Utkin <viktor_utkin@epam.com>
# Copyright: (c) 2019, Uladzimir Klybik <uladzimir_klybik@epam.com>
2017-08-29 00:11:10 +00:00
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2014-09-26 01:01:01 +00:00
2018-11-18 22:17:13 +00:00
#AnsibleRequires -CSharpUtil Ansible.Basic
# Requires -Module Ansible.ModuleUtils.AddType
2019-03-05 10:37:00 +00:00
# Requires -Module Ansible.ModuleUtils.FileUtil
2018-11-18 22:17:13 +00:00
$spec = @ {
options = @ {
url = @ { type = 'str' ; required = $true }
dest = @ { type = 'path' ; required = $true }
timeout = @ { type = 'int' ; default = 10 }
headers = @ { type = 'dict' ; default = @ { } }
validate_certs = @ { type = 'bool' ; default = $true }
url_username = @ { type = 'str' ; aliases = @ ( 'username' ) }
url_password = @ { type = 'str' ; aliases = @ ( 'password' ) ; no_log = $true }
force_basic_auth = @ { type = 'bool' ; default = $false }
use_proxy = @ { type = 'bool' ; default = $true }
proxy_url = @ { type = 'str' }
proxy_username = @ { type = 'str' }
proxy_password = @ { type = 'str' ; no_log = $true }
force = @ { type = 'bool' ; default = $true }
2019-03-05 10:37:00 +00:00
checksum = @ { type = 'str' }
checksum_algorithm = @ { type = 'str' ; default = 'sha1' ; choices = @ ( " md5 " , " sha1 " , " sha256 " , " sha384 " , " sha512 " ) }
checksum_url = @ { type = 'str' }
2018-11-18 22:17:13 +00:00
}
2019-03-05 10:37:00 +00:00
mutually_exclusive = @ (
, @ ( 'checksum' , 'checksum_url' )
)
2018-11-18 22:17:13 +00:00
supports_check_mode = $true
}
2017-08-29 00:11:10 +00:00
2018-11-18 22:17:13 +00:00
$module = [ Ansible.Basic.AnsibleModule ] :: Create ( $args , $spec )
$url = $module . Params . url
$dest = $module . Params . dest
$timeout = $module . Params . timeout
$headers = $module . Params . headers
$validate_certs = $module . Params . validate_certs
$url_username = $module . Params . url_username
$url_password = $module . Params . url_password
$force_basic_auth = $module . Params . force_basic_auth
$use_proxy = $module . Params . use_proxy
$proxy_url = $module . Params . proxy_url
$proxy_username = $module . Params . proxy_username
$proxy_password = $module . Params . proxy_password
$force = $module . Params . force
2019-03-05 10:37:00 +00:00
$checksum = $module . Params . checksum
$checksum_algorithm = $module . Params . checksum_algorithm
$checksum_url = $module . Params . checksum_url
2018-11-18 22:17:13 +00:00
2019-03-05 10:37:00 +00:00
$module . Result . elapsed = 0
$module . Result . url = $url
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
Function Invoke-AnsibleWebRequest {
<#
. SYNOPSIS
Creates a WebRequest and invokes a ScriptBlock with the response passed in .
It handles the common module options like credential , timeout , proxy options
in a single location to reduce code duplication .
#>
param (
[ Parameter ( Mandatory = $true ) ] [ Ansible.Basic.AnsibleModule ] $Module ,
[ Parameter ( Mandatory = $true ) ] [ Uri ] $Uri ,
[ Parameter ( Mandatory = $true ) ] [ Hashtable ] $Method ,
[ Parameter ( Mandatory = $true ) ] [ ScriptBlock ] $Script , # Invoked in this cmdlet
[ System.Collections.IDictionary ] $Headers ,
[ Int32 ] $Timeout ,
[ Switch ] $UseProxy ,
[ System.Net.WebProxy ] $Proxy ,
$Credential # Either a String (force_basic_auth) or NetCredentials
)
$web_request = [ System.Net.WebRequest ] :: Create ( $Uri )
$web_request . Method = $Method . ( $web_request . GetType ( ) . Name )
2018-08-31 20:20:56 +00:00
2017-08-29 00:11:10 +00:00
foreach ( $header in $headers . GetEnumerator ( ) ) {
2019-03-05 10:37:00 +00:00
$web_request . Headers . Add ( $header . Key , $header . Value )
2017-08-29 00:11:10 +00:00
}
if ( $timeout ) {
2019-03-05 10:37:00 +00:00
$web_request . Timeout = $timeout * 1000
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
if ( -not $UseProxy ) {
$web_request . Proxy = $null
} elseif ( $ProxyUri ) {
$web_request . Proxy = $Proxy
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
if ( $Credential ) {
if ( $Credential -is [ String ] ) {
# force_basic_auth=yes is set
$web_request . Headers . Add ( " Authorization " , " Basic $Credential " )
2018-05-03 22:39:37 +00:00
} else {
2019-03-05 10:37:00 +00:00
$web_request . Credentials = $Credential
2018-05-03 22:39:37 +00:00
}
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
try {
$web_response = $web_request . GetResponse ( )
$response_stream = $web_response . GetResponseStream ( )
try {
# Invoke the ScriptBlock and pass in the WebResponse and ResponseStream
& $Script -Response $web_response -Stream $response_stream
} finally {
$response_stream . Dispose ( )
}
if ( $Uri . IsFile ) {
# A FileWebResponse won't have these properties set
$module . Result . msg = " OK "
$module . Result . status_code = 200
} else {
$module . Result . msg = [ string ] $web_response . StatusDescription
$module . Result . status_code = [ int ] $web_response . StatusCode
}
} catch [ System.Net.WebException ] {
$module . Result . status_code = [ int ] $_ . Exception . Response . StatusCode
$module . FailJson ( " Error requesting ' $Uri '. $( $_ . Exception . Message ) " , $_ )
} finally {
if ( $web_response ) {
$web_response . Close ( )
}
2018-05-03 22:39:37 +00:00
}
2019-03-05 10:37:00 +00:00
}
Function Get-ChecksumFromUri {
param (
[ Parameter ( Mandatory = $true ) ] [ Ansible.Basic.AnsibleModule ] $Module ,
[ Parameter ( Mandatory = $true ) ] [ Uri ] $Uri ,
[ Uri ] $SourceUri ,
[ Hashtable ] $RequestParams
)
$script = {
param ( $Response , $Stream )
$read_stream = New-Object -TypeName System . IO . StreamReader -ArgumentList $Stream
$web_checksum = $read_stream . ReadToEnd ( )
$basename = ( Split-Path -Path $SourceUri . LocalPath -Leaf )
$basename = [ regex ] :: Escape ( $basename )
$web_checksum_str = $web_checksum -split '\r?\n' | Select-String -Pattern $ ( " \s+\.?\/?\\? " + $basename + " \s* $ " )
if ( -not $web_checksum_str ) {
$Module . FailJson ( " Checksum record not found for file name ' $basename ' in file from url: ' $Uri ' " )
}
$web_checksum_str_splitted = $web_checksum_str [ 0 ] . ToString ( ) . split ( " " , 2 )
$hash_from_file = $web_checksum_str_splitted [ 0 ] . Trim ( )
# Remove any non-alphanumeric characters
$hash_from_file = $hash_from_file -replace '\W+' , ''
2018-08-31 20:20:56 +00:00
2019-03-05 10:37:00 +00:00
Write-Output -InputObject $hash_from_file
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
$invoke_args = @ {
Module = $Module
Uri = $Uri
Method = @ {
FileWebRequest = [ System . Net . WebRequestMethods + File ] :: DownloadFile
FtpWebRequest = [ System . Net . WebRequestMethods + Ftp ] :: DownloadFile
HttpWebRequest = [ System . Net . WebRequestMethods + Http ] :: Get
}
Script = $script
}
try {
Invoke-AnsibleWebRequest @invoke_args @RequestParams
} catch {
$Module . FailJson ( " Error when getting the remote checksum from ' $Uri '. $( $_ . Exception . Message ) " , $_ )
}
}
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
Function Compare-ModifiedFile {
<#
. SYNOPSIS
Compares the remote URI resource against the local Dest resource . Will
return true if the LastWriteTime / LastModificationDate of the remote is
newer than the local resource date .
#>
param (
[ Parameter ( Mandatory = $true ) ] [ Ansible.Basic.AnsibleModule ] $Module ,
[ Parameter ( Mandatory = $true ) ] [ Uri ] $Uri ,
[ Parameter ( Mandatory = $true ) ] [ String ] $Dest ,
[ Hashtable ] $RequestParams
)
$dest_last_mod = ( Get-AnsibleItem -Path $Dest ) . LastWriteTimeUtc
# If the URI is a file we don't need to go through the whole WebRequest
if ( $Uri . IsFile ) {
$src_last_mod = ( Get-AnsibleItem -Path $Uri . AbsolutePath ) . LastWriteTimeUtc
2017-08-29 00:11:10 +00:00
} else {
2019-03-05 10:37:00 +00:00
$invoke_args = @ {
Module = $Module
Uri = $Uri
Method = @ {
FtpWebRequest = [ System . Net . WebRequestMethods + Ftp ] :: GetDateTimestamp
HttpWebRequest = [ System . Net . WebRequestMethods + Http ] :: Head
}
Script = { param ( $Response , $Stream ) ; $Response . LastModified }
}
try {
$src_last_mod = Invoke-AnsibleWebRequest @invoke_args @RequestParams
} catch {
$Module . FailJson ( " Error when requesting 'Last-Modified' date from ' $Uri '. $( $_ . Exception . Message ) " , $_ )
}
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
# Return $true if the Uri LastModification date is newer than the Dest LastModification date
( ( Get-Date -Date $src_last_mod ) . ToUniversalTime ( ) -gt $dest_last_mod )
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
Function Get-Checksum {
param (
[ Parameter ( Mandatory = $true ) ] [ String ] $Path ,
[ String ] $Algorithm = " sha1 "
)
switch ( $Algorithm ) {
'md5' { $sp = New-Object -TypeName System . Security . Cryptography . MD5CryptoServiceProvider }
'sha1' { $sp = New-Object -TypeName System . Security . Cryptography . SHA1CryptoServiceProvider }
'sha256' { $sp = New-Object -TypeName System . Security . Cryptography . SHA256CryptoServiceProvider }
'sha384' { $sp = New-Object -TypeName System . Security . Cryptography . SHA384CryptoServiceProvider }
'sha512' { $sp = New-Object -TypeName System . Security . Cryptography . SHA512CryptoServiceProvider }
}
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
$fs = [ System.IO.File ] :: Open ( $Path , [ System.IO.Filemode ] :: Open , [ System.IO.FileAccess ] :: Read ,
[ System.IO.FileShare ] :: ReadWrite )
try {
$hash = [ System.BitConverter ] :: ToString ( $sp . ComputeHash ( $fs ) ) . Replace ( " - " , " " ) . ToLower ( )
} finally {
$fs . Dispose ( )
}
return $hash
}
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
Function Invoke-DownloadFile {
param (
[ Parameter ( Mandatory = $true ) ] [ Ansible.Basic.AnsibleModule ] $Module ,
[ Parameter ( Mandatory = $true ) ] [ Uri ] $Uri ,
[ Parameter ( Mandatory = $true ) ] [ String ] $Dest ,
[ String ] $Checksum ,
[ String ] $ChecksumAlgorithm ,
[ Hashtable ] $RequestParams
)
2018-08-31 20:20:56 +00:00
2017-08-29 00:11:10 +00:00
# Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message.
2019-03-05 10:37:00 +00:00
$dest_parent = Split-Path -LiteralPath $Dest
2017-08-29 00:11:10 +00:00
if ( -not ( Test-Path -LiteralPath $dest_parent -PathType Container ) ) {
2019-03-05 10:37:00 +00:00
$module . FailJson ( " The path ' $dest_parent ' does not exist for destination ' $Dest ', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs. " )
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
$download_script = {
param ( $Response , $Stream )
# Download the file to a temporary directory so we can compare it
$tmp_dest = Join-Path -Path $Module . Tmpdir -ChildPath ( [ System.IO.Path ] :: GetRandomFileName ( ) )
$fs = [ System.IO.File ] :: Create ( $tmp_dest )
try {
$Stream . CopyTo ( $fs )
$fs . Flush ( )
} finally {
$fs . Dispose ( )
}
$tmp_checksum = Get-Checksum -Path $tmp_dest -Algorithm $ChecksumAlgorithm
$Module . Result . checksum_src = $tmp_checksum
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
# If the checksum has been set, verify the checksum of the remote against the input checksum.
if ( $Checksum -and $Checksum -ne $tmp_checksum ) {
$Module . FailJson ( ( " The checksum for {0} did not match '{1}', it was '{2}' " -f $Uri , $Checksum , $tmp_checksum ) )
}
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
$download = $true
if ( Test-Path -LiteralPath $Dest ) {
# Validate the remote checksum against the existing downloaded file
$dest_checksum = Get-Checksum -Path $Dest -Algorithm $ChecksumAlgorithm
# If we don't need to download anything, save the dest checksum so we don't waste time calculating it
# again at the end of the script
if ( $dest_checksum -eq $tmp_checksum ) {
$download = $false
$Module . Result . checksum_dest = $dest_checksum
$Module . Result . size = ( Get-AnsibleItem -Path $Dest ) . Length
}
}
2017-08-29 00:11:10 +00:00
2019-03-05 10:37:00 +00:00
if ( $download ) {
2019-03-19 02:01:35 +00:00
Copy-Item -LiteralPath $tmp_dest -Destination $Dest -Force -WhatIf: $Module . CheckMode > $null
2019-03-05 10:37:00 +00:00
$Module . Result . changed = $true
2017-12-18 20:38:44 +00:00
}
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
$invoke_args = @ {
Module = $Module
Uri = $Uri
Method = @ {
FileWebRequest = [ System . Net . WebRequestMethods + File ] :: DownloadFile
FtpWebRequest = [ System . Net . WebRequestMethods + Ftp ] :: DownloadFile
HttpWebRequest = [ System . Net . WebRequestMethods + Http ] :: Get
2017-08-29 00:11:10 +00:00
}
2019-03-05 10:37:00 +00:00
Script = $download_script
2017-08-29 00:11:10 +00:00
}
2017-08-29 22:52:39 +00:00
2019-03-05 10:37:00 +00:00
$module_start = Get-Date
try {
Invoke-AnsibleWebRequest @invoke_args @RequestParams
} catch {
$Module . FailJson ( " Unknown error downloading ' $Uri ' to ' $Dest ': $( $_ . Exception . Message ) " , $_ )
} finally {
$Module . Result . elapsed = ( ( Get-Date ) - $module_start ) . TotalSeconds
}
2017-08-29 00:11:10 +00:00
}
if ( -not $use_proxy -and ( $proxy_url -or $proxy_username -or $proxy_password ) ) {
2018-11-18 22:17:13 +00:00
$module . Warn ( " Not using a proxy on request, however a 'proxy_url', 'proxy_username' or 'proxy_password' was defined. " )
2017-08-29 00:11:10 +00:00
}
$proxy = $null
if ( $proxy_url ) {
$proxy = New-Object System . Net . WebProxy ( $proxy_url , $true )
if ( $proxy_username -and $proxy_password ) {
$proxy_credential = New-Object System . Net . NetworkCredential ( $proxy_username , $proxy_password )
$proxy . Credentials = $proxy_credential
}
}
$credentials = $null
2018-05-03 22:39:37 +00:00
if ( $url_username ) {
2017-12-18 20:38:44 +00:00
if ( $force_basic_auth ) {
$credentials = [ convert ] :: ToBase64String ( [ System.Text.Encoding ] :: ASCII . GetBytes ( $url_username + " : " + $url_password ) )
} else {
2018-09-11 04:23:48 +00:00
$credentials = New-Object System . Net . NetworkCredential ( $url_username , $url_password )
2017-12-18 20:38:44 +00:00
}
2017-08-29 00:11:10 +00:00
}
2017-07-05 23:56:43 +00:00
if ( -not $validate_certs ) {
2017-08-29 00:11:10 +00:00
[ System.Net.ServicePointManager ] :: ServerCertificateValidationCallback = { $true }
2015-06-25 20:52:23 +00:00
}
2017-08-29 00:11:10 +00:00
# Use last part of url for dest file name if a directory is supplied for $dest
if ( Test-Path -LiteralPath $dest -PathType Container ) {
$uri = [ System.Uri ] $url
$basename = Split-Path -Path $uri . LocalPath -Leaf
if ( $uri . LocalPath -and $uri . LocalPath -ne '/' -and $basename ) {
$url_basename = Split-Path -Path $uri . LocalPath -Leaf
$dest = Join-Path -Path $dest -ChildPath $url_basename
} else {
$dest = Join-Path -Path $dest -ChildPath $uri . Host
2015-06-08 11:45:20 +00:00
}
2018-11-18 22:17:13 +00:00
# Ensure we have a string instead of a PS object to avoid serialization issues
$dest = $dest . ToString ( )
2017-08-29 00:11:10 +00:00
} elseif ( ( [ System.IO.Path ] :: GetFileName ( $dest ) ) -eq '' ) {
# We have a trailing path separator
2018-11-18 22:17:13 +00:00
$module . FailJson ( " The destination path ' $dest ' does not exist, or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs. " )
2016-03-18 08:41:54 +00:00
}
2019-03-05 10:37:00 +00:00
2018-11-18 22:17:13 +00:00
$module . Result . dest = $dest
2016-03-18 08:41:54 +00:00
2017-08-11 01:00:34 +00:00
# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
2018-08-31 20:20:56 +00:00
$security_protocols = [ Net.ServicePointManager ] :: SecurityProtocol -bor [ Net.SecurityProtocolType ] :: SystemDefault
2017-08-11 01:00:34 +00:00
if ( [ Net.SecurityProtocolType ] . GetMember ( " Tls11 " ) . Count -gt 0 ) {
2018-08-31 20:20:56 +00:00
$security_protocols = $security_protocols -bor [ Net.SecurityProtocolType ] :: Tls11
2017-08-11 01:00:34 +00:00
}
if ( [ Net.SecurityProtocolType ] . GetMember ( " Tls12 " ) . Count -gt 0 ) {
2018-08-31 20:20:56 +00:00
$security_protocols = $security_protocols -bor [ Net.SecurityProtocolType ] :: Tls12
2017-08-11 01:00:34 +00:00
}
2018-08-31 20:20:56 +00:00
[ Net.ServicePointManager ] :: SecurityProtocol = $security_protocols
2016-03-18 08:41:54 +00:00
2019-03-05 10:37:00 +00:00
$request_params = @ {
Credential = $credentials
Headers = $headers
Timeout = $timeout
UseProxy = $use_proxy
Proxy = $proxy
}
2016-03-18 08:41:54 +00:00
2019-03-05 10:37:00 +00:00
if ( $checksum ) {
2019-03-13 22:04:49 +00:00
$checksum = $checksum . Trim ( ) . ToLower ( )
2019-03-05 10:37:00 +00:00
}
if ( $checksum_algorithm ) {
2019-03-13 22:04:49 +00:00
$checksum_algorithm = $checksum_algorithm . Trim ( ) . ToLower ( )
2019-03-05 10:37:00 +00:00
}
if ( $checksum_url ) {
2019-03-13 22:04:49 +00:00
$checksum_url = $checksum_url . Trim ( )
2019-03-05 10:37:00 +00:00
}
2015-06-25 20:52:23 +00:00
2019-03-05 10:37:00 +00:00
# Check for case $checksum variable contain url. If yes, get file data from url and replace original value in $checksum
if ( $checksum_url ) {
$checksum_uri = [ System.Uri ] $checksum_url
if ( $checksum_uri . Scheme -notin @ ( " file " , " ftp " , " http " , " https " ) ) {
$module . FailJson ( " Unsupported 'checksum_url' value for ' $dest ': ' $checksum_url ' " )
}
2015-06-25 20:52:23 +00:00
2019-03-05 10:37:00 +00:00
$checksum = Get-ChecksumFromUri -Module $Module -Uri $checksum_uri -SourceUri $url -RequestParams $request_params
}
2016-03-18 08:41:54 +00:00
2019-03-05 10:37:00 +00:00
if ( $force -or -not ( Test-Path -LiteralPath $dest ) ) {
# force=yes or dest does not exist, download the file
# Note: Invoke-DownloadFile will compare the checksums internally if dest exists
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
-ChecksumAlgorithm $checksum_algorithm -RequestParams $request_params
} else {
# force=no, we want to check the last modified dates and only download if they don't match
$is_modified = Compare-ModifiedFile -Module $module -Uri $url -Dest $dest -RequestParams $request_params
2017-08-29 00:11:10 +00:00
if ( $is_modified ) {
2019-03-05 10:37:00 +00:00
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
-ChecksumAlgorithm $checksum_algorithm -RequestParams $request_params
}
}
2016-03-18 08:41:54 +00:00
2019-03-05 10:37:00 +00:00
if ( ( -not $module . Result . ContainsKey ( " checksum_dest " ) ) -and ( Test-Path -LiteralPath $dest ) ) {
# Calculate the dest file checksum if it hasn't already been done
$module . Result . checksum_dest = Get-Checksum -Path $dest -Algorithm $checksum_algorithm
$module . Result . size = ( Get-AnsibleItem -Path $dest ) . Length
2014-09-26 01:01:01 +00:00
}
2018-11-18 22:17:13 +00:00
$module . ExitJson ( )
2019-03-05 10:37:00 +00:00