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