ansible/test/runner/setup/windows-httptester.ps1
2018-10-16 16:51:33 -07:00

227 lines
9 KiB
PowerShell

<#
.SYNOPSIS
Designed to set a Windows host to connect to the httptester container running
on the Ansible host. This will setup the Windows host file and forward the
local ports to use this connection. This will continue to run in the background
until the script is deleted.
Run this with SSH with the -R arguments to foward ports 8080 and 8443 to the
httptester container.
.PARAMETER Hosts
A list of hostnames to add to the Windows hosts file for the httptester
container.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)][String[]]$Hosts
)
$ProgressPreference = "SilentlyContinue"
$ErrorActionPreference = "Stop"
$os_version = [Version](Get-Item -Path "$env:SystemRoot\System32\kernel32.dll").VersionInfo.ProductVersion
Write-Verbose -Message "Configuring HTTP Tester on Windows $os_version for '$($Hosts -join "', '")'"
Function Get-PmapperRuleBytes {
<#
.SYNOPSIS
Create the byte values that configures a rule in the PMapper configuration
file. This isn't really documented but because PMapper is only used for
Server 2008 R2 we will stick to 1 version and just live with the legacy
work for now.
.PARAMETER ListenPort
The port to listen on localhost, this will be forwarded to the host defined
by ConnectAddress and ConnectPort.
.PARAMETER ConnectAddress
The hostname or IP to map the traffic to.
.PARAMETER ConnectPort
This port of ConnectAddress to map the traffic to.
#>
param(
[Parameter(Mandatory=$true)][UInt16]$ListenPort,
[Parameter(Mandatory=$true)][String]$ConnectAddress,
[Parameter(Mandatory=$true)][Int]$ConnectPort
)
$connect_field = "$($ConnectAddress):$ConnectPort"
$connect_bytes = [System.Text.Encoding]::ASCII.GetBytes($connect_field)
$data_length = [byte]($connect_bytes.Length + 6) # size of payload minus header, length, and footer
$port_bytes = [System.BitConverter]::GetBytes($ListenPort)
$payload = [System.Collections.Generic.List`1[Byte]]@()
$payload.Add([byte]16) > $null # header is \x10, means Configure Mapping rule
$payload.Add($data_length) > $null
$payload.AddRange($connect_bytes)
$payload.AddRange($port_bytes)
$payload.AddRange([byte[]]@(0, 0)) # 2 extra bytes of padding
$payload.Add([byte]0) > $null # 0 is TCP, 1 is UDP
$payload.Add([byte]0) > $null # 0 is Any, 1 is Internet
$payload.Add([byte]31) > $null # footer is \x1f, means end of Configure Mapping rule
return ,$payload.ToArray()
}
Write-Verbose -Message "Adding host file entries"
$hosts_file = "$env:SystemRoot\System32\drivers\etc\hosts"
$hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
$changed = $false
foreach ($httptester_host in $Hosts) {
$host_line = "127.0.0.1 $httptester_host # ansible-test httptester"
if ($host_line -notin $hosts_file_lines) {
$hosts_file_lines += $host_line
$changed = $true
}
}
if ($changed) {
Write-Verbose -Message "Host file is missing entries, adding missing entries"
[System.IO.File]::WriteAllLines($hosts_file, $hosts_file_lines)
}
# forward ports
$forwarded_ports = @{
80 = 8080
443 = 8443
}
if ($os_version -ge [Version]"6.2") {
Write-Verbose -Message "Using netsh to configure forwarded ports"
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
$port_set = netsh interface portproxy show v4tov4 | `
Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
if (-not $port_set) {
Write-Verbose -Message "Adding netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$add_args = @(
"interface",
"portproxy",
"add",
"v4tov4",
"listenaddress=127.0.0.1",
"listenport=$($forwarded_port.Key)",
"connectaddress=127.0.0.1",
"connectport=$($forwarded_port.Value)"
)
$null = netsh $add_args 2>&1
}
}
} else {
Write-Verbose -Message "Using Port Mapper to configure forwarded ports"
# netsh interface portproxy doesn't work on local addresses in older
# versions of Windows. Use custom application Port Mapper to acheive the
# same outcome
# http://www.analogx.com/contents/download/Network/pmapper/Freeware.htm
$s3_url = "https://s3.amazonaws.com/ansible-ci-files/ansible-test/pmapper-1.04.exe"
# download the Port Mapper executable to a temporary directory
$pmapper_folder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
$pmapper_exe = Join-Path -Path $pmapper_folder -ChildPath pmapper.exe
$pmapper_config = Join-Path -Path $pmapper_folder -ChildPath pmapper.dat
New-Item -Path $pmapper_folder -ItemType Directory > $null
$stop = $false
do {
try {
Write-Verbose -Message "Attempting download of '$s3_url'"
(New-Object -TypeName System.Net.WebClient).DownloadFile($s3_url, $pmapper_exe)
$stop = $true
} catch { Start-Sleep -Second 5 }
} until ($stop)
# create the Port Mapper rule file that contains our forwarded ports
$fs = [System.IO.File]::Create($pmapper_config)
try {
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
Write-Verbose -Message "Creating forwarded port rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$pmapper_rule = Get-PmapperRuleBytes -ListenPort $forwarded_port.Key -ConnectAddress 127.0.0.1 -ConnectPort $forwarded_port.Value
$fs.Write($pmapper_rule, 0, $pmapper_rule.Length)
}
} finally {
$fs.Close()
}
Write-Verbose -Message "Starting Port Mapper '$pmapper_exe' in the background"
$start_args = @{
CommandLine = $pmapper_exe
CurrentDirectory = $pmapper_folder
}
$res = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments $start_args
if ($res.ReturnValue -ne 0) {
$error_msg = switch($res.ReturnValue) {
2 { "Access denied" }
3 { "Insufficient privilege" }
8 { "Unknown failure" }
9 { "Path not found" }
21 { "Invalid parameter" }
default { "Undefined Error: $($res.ReturnValue)" }
}
Write-Error -Message "Failed to start pmapper: $error_msg"
}
$pmapper_pid = $res.ProcessId
Write-Verbose -Message "Port Mapper PID: $pmapper_pid"
}
Write-Verbose -Message "Wait for current script at '$PSCommandPath' to be deleted before running cleanup"
$fsw = New-Object -TypeName System.IO.FileSystemWatcher
$fsw.Path = Split-Path -Path $PSCommandPath -Parent
$fsw.Filter = Split-Path -Path $PSCommandPath -Leaf
$fsw.WaitForChanged([System.IO.WatcherChangeTypes]::Deleted, 3600000) > $null
Write-Verbose -Message "Script delete or timeout reached, cleaning up Windows httptester artifacts"
Write-Verbose -Message "Cleanup host file entries"
$hosts_file_lines = [System.IO.File]::ReadAllLines($hosts_file)
$new_lines = [System.Collections.ArrayList]@()
$changed = $false
foreach ($host_line in $hosts_file_lines) {
if ($host_line.EndsWith("# ansible-test httptester")) {
$changed = $true
continue
}
$new_lines.Add($host_line) > $null
}
if ($changed) {
Write-Verbose -Message "Host file has extra entries, removing extra entries"
[System.IO.File]::WriteAllLines($hosts_file, $new_lines)
}
if ($os_version -ge [Version]"6.2") {
Write-Verbose -Message "Cleanup of forwarded port configured in netsh"
foreach ($forwarded_port in $forwarded_ports.GetEnumerator()) {
$port_set = netsh interface portproxy show v4tov4 | `
Where-Object { $_ -match "127.0.0.1\s*$($forwarded_port.Key)\s*127.0.0.1\s*$($forwarded_port.Value)" }
if ($port_set) {
Write-Verbose -Message "Removing netsh portproxy rule for $($forwarded_port.Key) -> $($forwarded_port.Value)"
$delete_args = @(
"interface",
"portproxy",
"delete",
"v4tov4",
"listenaddress=127.0.0.1",
"listenport=$($forwarded_port.Key)"
)
$null = netsh $delete_args 2>&1
}
}
} else {
Write-Verbose -Message "Stopping Port Mapper executable based on pid $pmapper_pid"
Stop-Process -Id $pmapper_pid -Force
# the process may not stop straight away, try multiple times to delete the Port Mapper folder
$attempts = 1
do {
try {
Write-Verbose -Message "Cleanup temporary files for Port Mapper at '$pmapper_folder' - Attempt: $attempts"
Remove-Item -Path $pmapper_folder -Force -Recurse
break
} catch {
Write-Verbose -Message "Cleanup temporary files for Port Mapper failed, waiting 5 seconds before trying again:$($_ | Out-String)"
if ($attempts -ge 5) {
break
}
$attempts += 1
Start-Sleep -Second 5
}
} until ($true)
}