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