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