1 <#
2 .SYNOPSIS
3 Designed to set a Windows host to connect to the httptester container running
4 on the Ansible host. This will setup the Windows host file and forward the
5 local ports to use this connection. This will continue to run in the background
6 until the script is deleted.
7
8 Run this with SSH with the -R arguments to forward ports 8080, 8443 and 8444 to the
9 httptester container.
10
11 .PARAMETER Hosts
12 A list of hostnames, delimited by '|', to add to the Windows hosts file for the
13 httptester container, e.g. 'ansible.host.com|secondary.host.test'.
14 #>
15 [CmdletBinding()]
16 param(
17 [Parameter(Mandatory=$true, Position=0)][String]$Hosts
18 )
19 $Hosts = $Hosts.Split('|')
20
21 $ProgressPreference = "SilentlyContinue"
22 $ErrorActionPreference = "Stop"
23 $os_version = [Version](Get-Item -Path "$env:SystemRoot\System32\kernel32.dll").VersionInfo.ProductVersion
24 Write-Verbose -Message "Configuring HTTP Tester on Windows $os_version for '$($Hosts -join "', '")'"
25
Get-PmapperRuleBytes()26 Function Get-PmapperRuleBytes {
27 <#
28 .SYNOPSIS
29 Create the byte values that configures a rule in the PMapper configuration
30 file. This isn't really documented but because PMapper is only used for
31 Server 2008 R2 we will stick to 1 version and just live with the legacy
32 work for now.
33
34 .PARAMETER ListenPort
35 The port to listen on localhost, this will be forwarded to the host defined
36 by ConnectAddress and ConnectPort.
37
38 .PARAMETER ConnectAddress
39 The hostname or IP to map the traffic to.
40
41 .PARAMETER ConnectPort
42 This port of ConnectAddress to map the traffic to.
43 #>
44 param(
45 [Parameter(Mandatory=$true)][UInt16]$ListenPort,
46 [Parameter(Mandatory=$true)][String]$ConnectAddress,
47 [Parameter(Mandatory=$true)][Int]$ConnectPort
48 )
49
50 $connect_field = "$($ConnectAddress):$ConnectPort"
51 $connect_bytes = [System.Text.Encoding]::ASCII.GetBytes($connect_field)
52 $data_length = [byte]($connect_bytes.Length + 6) # size of payload minus header, length, and footer
53 $port_bytes = [System.BitConverter]::GetBytes($ListenPort)
54
55 $payload = [System.Collections.Generic.List`1[Byte]]@()
56 $payload.Add([byte]16) > $null # header is \x10, means Configure Mapping rule
57 $payload.Add($data_length) > $null
58 $payload.AddRange($connect_bytes)
59 $payload.AddRange($port_bytes)
60 $payload.AddRange([byte[]]@(0, 0)) # 2 extra bytes of padding
61 $payload.Add([byte]0) > $null # 0 is TCP, 1 is UDP
62 $payload.Add([byte]0) > $null # 0 is Any, 1 is Internet
63 $payload.Add([byte]31) > $null # footer is \x1f, means end of Configure Mapping rule
64
65 return ,$payload.ToArray()
66 }
67
68 Write-Verbose -Message "Adding host file entries"
69 $hosts_file = "$env:SystemRoot\System32\drivers\etc\hosts"
70 $hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
71 $changed = $false
72 foreach ($httptester_host in $Hosts) {
73 $host_line = "127.0.0.1 $httptester_host # ansible-test httptester"
74 if ($host_line -notin $hosts_file_lines) {
75 $hosts_file_lines += $host_line
76 $changed = $true
77 }
78 }
79 if ($changed) {
80 Write-Verbose -Message "Host file is missing entries, adding missing entries"
81 [System.IO.File]::WriteAllLines($hosts_file, $hosts_file_lines)
82 }
83
84 # forward ports
85 $forwarded_ports = @{
86 80 = 8080
87 443 = 8443
88 444 = 8444
89 }
90 if ($os_version -ge [Version]"6.2") {
91 Write-Verbose -Message "Using netsh to configure forwarded ports"
92 foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
93 $port_set = netsh interface portproxy show v4tov4 | `
94 Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
95
96 if (-not $port_set) {
97 Write-Verbose -Message "Adding netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
98 $add_args = @(
99 "interface",
100 "portproxy",
101 "add",
102 "v4tov4",
103 "listenaddress=127.0.0.1",
104 "listenport=$($forwarded_port.Key)",
105 "connectaddress=127.0.0.1",
106 "connectport=$($forwarded_port.Value)"
107 )
108 $null = netsh $add_args 2>&1
109 }
110 }
111 } else {
112 Write-Verbose -Message "Using Port Mapper to configure forwarded ports"
113 # netsh interface portproxy doesn't work on local addresses in older
114 # versions of Windows. Use custom application Port Mapper to acheive the
115 # same outcome
116 # http://www.analogx.com/contents/download/Network/pmapper/Freeware.htm
117 $s3_url = "https://ansible-ci-files.s3.amazonaws.com/ansible-test/pmapper-1.04.exe"
118
119 # download the Port Mapper executable to a temporary directory
120 $pmapper_folder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
121 $pmapper_exe = Join-Path -Path $pmapper_folder -ChildPath pmapper.exe
122 $pmapper_config = Join-Path -Path $pmapper_folder -ChildPath pmapper.dat
123 New-Item -Path $pmapper_folder -ItemType Directory > $null
124
125 $stop = $false
126 do {
127 try {
128 Write-Verbose -Message "Attempting download of '$s3_url'"
129 (New-Object -TypeName System.Net.WebClient).DownloadFile($s3_url, $pmapper_exe)
130 $stop = $true
131 } catch { Start-Sleep -Second 5 }
132 } until ($stop)
133
134 # create the Port Mapper rule file that contains our forwarded ports
135 $fs = [System.IO.File]::Create($pmapper_config)
136 try {
137 foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
138 Write-Verbose -Message "Creating forwarded port rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
139 $pmapper_rule = Get-PmapperRuleBytes -ListenPort $forwarded_port.Key -ConnectAddress 127.0.0.1 -ConnectPort $forwarded_port.Value
140 $fs.Write($pmapper_rule, 0, $pmapper_rule.Length)
141 }
142 } finally {
143 $fs.Close()
144 }
145
146 Write-Verbose -Message "Starting Port Mapper '$pmapper_exe' in the background"
147 $start_args = @{
148 CommandLine = $pmapper_exe
149 CurrentDirectory = $pmapper_folder
150 }
151 $res = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments $start_args
152 if ($res.ReturnValue -ne 0) {
153 $error_msg = switch($res.ReturnValue) {
154 2 { "Access denied" }
155 3 { "Insufficient privilege" }
156 8 { "Unknown failure" }
157 9 { "Path not found" }
158 21 { "Invalid parameter" }
159 default { "Undefined Error: $($res.ReturnValue)" }
160 }
161 Write-Error -Message "Failed to start pmapper: $error_msg"
162 }
163 $pmapper_pid = $res.ProcessId
164 Write-Verbose -Message "Port Mapper PID: $pmapper_pid"
165 }
166
167 Write-Verbose -Message "Wait for current script at '$PSCommandPath' to be deleted before running cleanup"
168 $fsw = New-Object -TypeName System.IO.FileSystemWatcher
169 $fsw.Path = Split-Path -Path $PSCommandPath -Parent
170 $fsw.Filter = Split-Path -Path $PSCommandPath -Leaf
171 $fsw.WaitForChanged([System.IO.WatcherChangeTypes]::Deleted, 3600000) > $null
172 Write-Verbose -Message "Script delete or timeout reached, cleaning up Windows httptester artifacts"
173
174 Write-Verbose -Message "Cleanup host file entries"
175 $hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
176 $new_lines = [System.Collections.ArrayList]@()
177 $changed = $false
178 foreach ($host_line in $hosts_file_lines) {
179 if ($host_line.EndsWith("# ansible-test httptester")) {
180 $changed = $true
181 continue
182 }
183 $new_lines.Add($host_line) > $null
184 }
185 if ($changed) {
186 Write-Verbose -Message "Host file has extra entries, removing extra entries"
187 [System.IO.File]::WriteAllLines($hosts_file, $new_lines)
188 }
189
190 if ($os_version -ge [Version]"6.2") {
191 Write-Verbose -Message "Cleanup of forwarded port configured in netsh"
192 foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
193 $port_set = netsh interface portproxy show v4tov4 | `
194 Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
195
196 if ($port_set) {
197 Write-Verbose -Message "Removing netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
198 $delete_args = @(
199 "interface",
200 "portproxy",
201 "delete",
202 "v4tov4",
203 "listenaddress=127.0.0.1",
204 "listenport=$($forwarded_port.Key)"
205 )
206 $null = netsh $delete_args 2>&1
207 }
208 }
209 } else {
210 Write-Verbose -Message "Stopping Port Mapper executable based on pid $pmapper_pid"
211 Stop-Process -Id $pmapper_pid -Force
212
213 # the process may not stop straight away, try multiple times to delete the Port Mapper folder
214 $attempts = 1
215 do {
216 try {
217 Write-Verbose -Message "Cleanup temporary files for Port Mapper at '$pmapper_folder' - Attempt: $attempts"
218 Remove-Item -Path $pmapper_folder -Force -Recurse
219 break
220 } catch {
221 Write-Verbose -Message "Cleanup temporary files for Port Mapper failed, waiting 5 seconds before trying again:$($_ | Out-String)"
222 if ($attempts -ge 5) {
223 break
224 }
225 $attempts += 1
226 Start-Sleep -Second 5
227 }
228 } until ($true)
229 }
230