|
| 1 | +#!powershell |
| 2 | + |
| 3 | +# Copyright: (c) 2017, Noah Sparks <[email protected]> |
| 4 | +# Copyright: (c) 2015, Henrik Wallström <[email protected]> |
| 5 | +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) |
| 6 | + |
| 7 | +#Requires -Module Ansible.ModuleUtils.Legacy |
| 8 | + |
| 9 | +$params = Parse-Args -arguments $args -supports_check_mode $true |
| 10 | +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false |
| 11 | + |
| 12 | +$name = Get-AnsibleParam $params -name "name" -type str -failifempty $true -aliases 'website' |
| 13 | +$state = Get-AnsibleParam $params "state" -default "present" -validateSet "present", "absent" |
| 14 | +$host_header = Get-AnsibleParam $params -name "host_header" -type str |
| 15 | +$protocol = Get-AnsibleParam $params -name "protocol" -type str -default 'http' |
| 16 | +$port = Get-AnsibleParam $params -name "port" -default '80' |
| 17 | +$ip = Get-AnsibleParam $params -name "ip" -default '*' |
| 18 | +$certificateHash = Get-AnsibleParam $params -name "certificate_hash" -type str -default ([string]::Empty) |
| 19 | +$certificateStoreName = Get-AnsibleParam $params -name "certificate_store_name" -type str -default ([string]::Empty) |
| 20 | +$sslFlags = Get-AnsibleParam $params -name "ssl_flags" -default '0' -ValidateSet '0', '1', '2', '3' |
| 21 | + |
| 22 | +$result = @{ |
| 23 | + changed = $false |
| 24 | +} |
| 25 | + |
| 26 | +################# |
| 27 | +### Functions ### |
| 28 | +################# |
| 29 | +function New-BindingInfo { |
| 30 | + $ht = @{ |
| 31 | + 'bindingInformation' = $args[0].bindingInformation |
| 32 | + 'ip' = $args[0].bindingInformation.split(':')[0] |
| 33 | + 'port' = [int]$args[0].bindingInformation.split(':')[1] |
| 34 | + 'hostheader' = $args[0].bindingInformation.split(':')[2] |
| 35 | + #'isDsMapperEnabled' = $args[0].isDsMapperEnabled |
| 36 | + 'protocol' = $args[0].protocol |
| 37 | + 'certificateStoreName' = $args[0].certificateStoreName |
| 38 | + 'certificateHash' = $args[0].certificateHash |
| 39 | + } |
| 40 | + |
| 41 | + #handle sslflag support |
| 42 | + If ([version][System.Environment]::OSVersion.Version -lt [version]'6.2') { |
| 43 | + $ht.sslFlags = 'not supported' |
| 44 | + } |
| 45 | + Else { |
| 46 | + $ht.sslFlags = [int]$args[0].sslFlags |
| 47 | + } |
| 48 | + |
| 49 | + Return $ht |
| 50 | +} |
| 51 | + |
| 52 | +# Used instead of get-webbinding to ensure we always return a single binding |
| 53 | +# We can't filter properly with get-webbinding...ex get-webbinding ip * returns all bindings |
| 54 | +# pass it $binding_parameters hashtable |
| 55 | +function Get-SingleWebBinding { |
| 56 | + |
| 57 | + Try { |
| 58 | + $site_bindings = get-webbinding -name $args[0].name |
| 59 | + } |
| 60 | + Catch { |
| 61 | + # 2k8r2 throws this error when you run get-webbinding with no bindings in iis |
| 62 | + $msg = 'Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value' |
| 63 | + If (-not $_.Exception.Message.CompareTo($msg)) { |
| 64 | + Throw $_.Exception.Message |
| 65 | + } |
| 66 | + Else { return } |
| 67 | + } |
| 68 | + |
| 69 | + Foreach ($binding in $site_bindings) { |
| 70 | + $splits = $binding.bindingInformation -split ':' |
| 71 | + |
| 72 | + if ( |
| 73 | + $args[0].protocol -eq $binding.protocol -and |
| 74 | + $args[0].ipaddress -eq $splits[0] -and |
| 75 | + $args[0].port -eq $splits[1] -and |
| 76 | + $args[0].hostheader -eq $splits[2] |
| 77 | + ) { |
| 78 | + Return $binding |
| 79 | + } |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | + |
| 84 | +############################# |
| 85 | +### Pre-Action Validation ### |
| 86 | +############################# |
| 87 | +$os_version = [version][System.Environment]::OSVersion.Version |
| 88 | + |
| 89 | +# Ensure WebAdministration module is loaded |
| 90 | +If ($os_version -lt [version]'6.1') { |
| 91 | + Try { |
| 92 | + Add-PSSnapin WebAdministration |
| 93 | + } |
| 94 | + Catch { |
| 95 | + Fail-Json -obj $result -message "The WebAdministration snap-in is not present. Please make sure it is installed." |
| 96 | + } |
| 97 | +} |
| 98 | +Else { |
| 99 | + Try { |
| 100 | + Import-Module WebAdministration |
| 101 | + } |
| 102 | + Catch { |
| 103 | + Fail-Json -obj $result -message "Failed to load WebAdministration module. Is IIS installed? $($_.Exception.Message)" |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +# ensure website targetted exists. -Name filter doesn't work on 2k8r2 so do where-object instead |
| 108 | +$website_check = get-website | Where-Object { $_.name -eq $name } |
| 109 | +If (-not $website_check) { |
| 110 | + Fail-Json -obj $result -message "Unable to retrieve website with name $Name. Make sure the website name is valid and exists." |
| 111 | +} |
| 112 | + |
| 113 | +# if OS older than 2012 (6.2) and ssl flags are set, fail. Otherwise toggle sni_support |
| 114 | +If ($os_version -lt [version]'6.2') { |
| 115 | + If ($sslFlags -ne 0) { |
| 116 | + Fail-Json -obj $result -message "SNI and Certificate Store support is not available for systems older than 2012 (6.2)" |
| 117 | + } |
| 118 | + $sni_support = $false #will cause the sslflags check later to skip |
| 119 | +} |
| 120 | +Else { |
| 121 | + $sni_support = $true |
| 122 | +} |
| 123 | + |
| 124 | +# make sure ssl flags only specified with https protocol |
| 125 | +If ($protocol -ne 'https' -and $sslFlags -gt 0) { |
| 126 | + Fail-Json -obj $result -message "SSLFlags can only be set for HTTPS protocol" |
| 127 | +} |
| 128 | + |
| 129 | +# validate certificate details if provided |
| 130 | +# we don't do anything with cert on state: absent, so only validate present |
| 131 | +If ($certificateHash -and $state -eq 'present') { |
| 132 | + If ($protocol -ne 'https') { |
| 133 | + Fail-Json -obj $result -message "You can only provide a certificate thumbprint when protocol is set to https" |
| 134 | + } |
| 135 | + |
| 136 | + #apply default for cert store name |
| 137 | + If (-Not $certificateStoreName) { |
| 138 | + $certificateStoreName = 'my' |
| 139 | + } |
| 140 | + |
| 141 | + #validate cert path |
| 142 | + $cert_path = "cert:\LocalMachine\$certificateStoreName\$certificateHash" |
| 143 | + If (-Not (Test-Path -LiteralPath $cert_path) ) { |
| 144 | + Fail-Json -obj $result -message "Unable to locate certificate at $cert_path" |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +# make sure binding info is valid for central cert store if sslflags -gt 1 |
| 149 | +If ($sslFlags -gt 1 -and ($certificateHash -ne [string]::Empty -or $certificateStoreName -ne [string]::Empty)) { |
| 150 | + Fail-Json -obj $result -message "You set sslFlags to $sslFlags. This indicates you wish to use the Central Certificate Store feature. |
| 151 | + This cannot be used in combination with certficiate_hash and certificate_store_name. When using the Central Certificate Store feature, |
| 152 | + the certificate is automatically retrieved from the store rather than manually assigned to the binding." |
| 153 | +} |
| 154 | + |
| 155 | +# disallow host_header: '*' |
| 156 | +If ($host_header -eq '*') { |
| 157 | + Fail-Json -obj $result -message "To make or remove a catch-all binding, please omit the host_header parameter entirely rather than specify host_header *" |
| 158 | +} |
| 159 | + |
| 160 | +########################## |
| 161 | +### start action items ### |
| 162 | +########################## |
| 163 | + |
| 164 | +# create binding search splat |
| 165 | +$binding_parameters = @{ |
| 166 | + Name = $name |
| 167 | + Protocol = $protocol |
| 168 | + Port = $port |
| 169 | + IPAddress = $ip |
| 170 | +} |
| 171 | + |
| 172 | +# insert host header to search if specified, otherwise it will return * (all bindings matching protocol/ip) |
| 173 | +If ($host_header) { |
| 174 | + $binding_parameters.HostHeader = $host_header |
| 175 | +} |
| 176 | +Else { |
| 177 | + $binding_parameters.HostHeader = [string]::Empty |
| 178 | +} |
| 179 | + |
| 180 | +# Get bindings matching parameters |
| 181 | +Try { |
| 182 | + $current_bindings = Get-SingleWebBinding $binding_parameters |
| 183 | +} |
| 184 | +Catch { |
| 185 | + Fail-Json -obj $result -message "Failed to retrieve bindings with Get-SingleWebBinding - $($_.Exception.Message)" |
| 186 | +} |
| 187 | + |
| 188 | +################################################ |
| 189 | +### Remove binding or exit if already absent ### |
| 190 | +################################################ |
| 191 | +If ($current_bindings -and $state -eq 'absent') { |
| 192 | + Try { |
| 193 | + #there is a bug in this method that will result in all bindings being removed if the IP in $current_bindings is a * |
| 194 | + #$current_bindings | Remove-WebBinding -verbose -WhatIf:$check_mode |
| 195 | + |
| 196 | + #another method that did not work. It kept failing to match on element and removed everything. |
| 197 | + #$element = @{protocol="$protocol";bindingInformation="$ip`:$port`:$host_header"} |
| 198 | + #Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtElement $element -WhatIf #:$check_mode |
| 199 | + |
| 200 | + #this method works |
| 201 | + [array]$bindings = Get-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection |
| 202 | + |
| 203 | + $index = Foreach ($item in $bindings) { |
| 204 | + If ( $protocol -eq $item.protocol -and $current_bindings.bindingInformation -eq $item.bindingInformation ) { |
| 205 | + $bindings.indexof($item) |
| 206 | + break |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtIndex $index -WhatIf:$check_mode |
| 211 | + $result.changed = $true |
| 212 | + } |
| 213 | + |
| 214 | + Catch { |
| 215 | + Fail-Json -obj $result -message "Failed to remove the binding from IIS - $($_.Exception.Message)" |
| 216 | + } |
| 217 | + |
| 218 | + # removing bindings from iis may not also remove them from iis:\sslbindings |
| 219 | + |
| 220 | + $result.operation_type = 'removed' |
| 221 | + $result.binding_info = $current_bindings | ForEach-Object { New-BindingInfo $_ } |
| 222 | + Exit-Json -obj $result |
| 223 | +} |
| 224 | +ElseIf (-Not $current_bindings -and $state -eq 'absent') { |
| 225 | + # exit changed: false since it's already gone |
| 226 | + Exit-Json -obj $result |
| 227 | +} |
| 228 | + |
| 229 | + |
| 230 | +################################ |
| 231 | +### Modify existing bindings ### |
| 232 | +################################ |
| 233 | +<# |
| 234 | +since we have already have the parameters available to get-webbinding, |
| 235 | +we just need to check here for the ones that are not available which are the |
| 236 | +ssl settings (hash, store, sslflags). If they aren't set we update here, or |
| 237 | +exit with changed: false |
| 238 | +#> |
| 239 | +ElseIf ($current_bindings) { |
| 240 | + #ran into a strange edge case in testing where I was able to retrieve bindings but not expand all the properties |
| 241 | + #when adding a self-signed wildcard cert to a binding. it seemed to permanently break the binding. only removing it |
| 242 | + #would cause the error to stop. |
| 243 | + Try { |
| 244 | + $null = $current_bindings | Select-Object * |
| 245 | + } |
| 246 | + Catch { |
| 247 | + $msg = -join @( |
| 248 | + "Found a matching binding, but failed to expand it's properties (get-binding | FL *). " |
| 249 | + "In testing, this was caused by using a self-signed wildcard certificate. $($_.Exception.Message)" |
| 250 | + ) |
| 251 | + Fail-Json -obj $result -message $msg |
| 252 | + } |
| 253 | + |
| 254 | + # check if there is a match on the ssl parameters |
| 255 | + If ( ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) -or |
| 256 | + $current_bindings.certificateHash -ne $certificateHash -or |
| 257 | + $current_bindings.certificateStoreName -ne $certificateStoreName) { |
| 258 | + # match/update SNI |
| 259 | + If ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) { |
| 260 | + Try { |
| 261 | + Set-WebBinding -Name $name -IPAddress $ip -Port $port -HostHeader $host_header -PropertyName sslFlags -value $sslFlags -whatif:$check_mode |
| 262 | + $result.changed = $true |
| 263 | + } |
| 264 | + Catch { |
| 265 | + Fail-Json -obj $result -message "Failed to update sslFlags on binding - $($_.Exception.Message)" |
| 266 | + } |
| 267 | + |
| 268 | + # Refresh the binding object since it has been changed |
| 269 | + Try { |
| 270 | + $current_bindings = Get-SingleWebBinding $binding_parameters |
| 271 | + } |
| 272 | + Catch { |
| 273 | + Fail-Json -obj $result -message "Failed to refresh bindings after setting sslFlags - $($_.Exception.Message)" |
| 274 | + } |
| 275 | + } |
| 276 | + # match/update certificate |
| 277 | + If ($current_bindings.certificateHash -ne $certificateHash -or $current_bindings.certificateStoreName -ne $certificateStoreName) { |
| 278 | + If (-Not $check_mode) { |
| 279 | + Try { |
| 280 | + $current_bindings.AddSslCertificate($certificateHash, $certificateStoreName) |
| 281 | + } |
| 282 | + Catch { |
| 283 | + Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)" |
| 284 | + } |
| 285 | + } |
| 286 | + } |
| 287 | + $result.changed = $true |
| 288 | + $result.operation_type = 'updated' |
| 289 | + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State |
| 290 | + $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters) |
| 291 | + Exit-Json -obj $result #exit changed true |
| 292 | + } |
| 293 | + Else { |
| 294 | + $result.operation_type = 'matched' |
| 295 | + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State |
| 296 | + $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters) |
| 297 | + Exit-Json -obj $result #exit changed false |
| 298 | + } |
| 299 | +} |
| 300 | + |
| 301 | +######################## |
| 302 | +### Add new bindings ### |
| 303 | +######################## |
| 304 | +ElseIf (-not $current_bindings -and $state -eq 'present') { |
| 305 | + # add binding. this creates the binding, but does not apply a certificate to it. |
| 306 | + Try { |
| 307 | + If (-not $check_mode) { |
| 308 | + If ($sni_support) { |
| 309 | + New-WebBinding @binding_parameters -SslFlags $sslFlags -Force |
| 310 | + } |
| 311 | + Else { |
| 312 | + New-WebBinding @binding_parameters -Force |
| 313 | + } |
| 314 | + } |
| 315 | + $result.changed = $true |
| 316 | + } |
| 317 | + Catch { |
| 318 | + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State |
| 319 | + Fail-Json -obj $result -message "Failed at creating new binding (note: creating binding and adding ssl are separate steps) - $($_.Exception.Message)" |
| 320 | + } |
| 321 | + |
| 322 | + # add certificate to binding |
| 323 | + If ($certificateHash -and -not $check_mode) { |
| 324 | + Try { |
| 325 | + #$new_binding = get-webbinding -Name $name -IPAddress $ip -port $port -Protocol $protocol -hostheader $host_header |
| 326 | + $new_binding = Get-SingleWebBinding $binding_parameters |
| 327 | + $new_binding.addsslcertificate($certificateHash, $certificateStoreName) |
| 328 | + } |
| 329 | + Catch { |
| 330 | + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State |
| 331 | + Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)" |
| 332 | + } |
| 333 | + } |
| 334 | + |
| 335 | + $result.changed = $true |
| 336 | + $result.operation_type = 'added' |
| 337 | + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State |
| 338 | + |
| 339 | + # incase there are no bindings we do a check before calling New-BindingInfo |
| 340 | + $web_binding = Get-SingleWebBinding $binding_parameters |
| 341 | + if ($web_binding) { |
| 342 | + $result.binding_info = New-BindingInfo $web_binding |
| 343 | + } |
| 344 | + else { |
| 345 | + $result.binding_info = $null |
| 346 | + } |
| 347 | + Exit-Json $result |
| 348 | +} |
0 commit comments