q3rcon-ps/lib/base.ps1

206 lines
7.2 KiB
PowerShell

class Base {
static [int]$DEFAULT_RECEIVE_TIMEOUT = 100
static [int]$DEFAULT_BUFFER_SIZE = 4096
static [int]$DEFAULT_SEND_TIMEOUT = 5000
[string]$hostname
[int]$port
[string]$passwd
[Object]$request
[Object]$response
hidden [System.Net.Sockets.Socket] $_socket
hidden [bool]$_disposed = $false
hidden [byte[]]$_receiveBuffer
Base ([string]$hostname, [int]$port, [string]$passwd) {
if ([string]::IsNullOrWhiteSpace($hostname)) {
throw [System.ArgumentException]::new('Hostname cannot be null or empty', 'hostname')
}
if ($port -le 0 -or $port -gt 65535) {
throw [System.ArgumentOutOfRangeException]::new('port', 'Port must be between 1-65535')
}
if ([string]::IsNullOrWhiteSpace($passwd)) {
throw [System.ArgumentException]::new('Password cannot be null or empty', 'passwd')
}
$this.hostname = $hostname
$this.port = $port
$this.passwd = $passwd
$this.request = New-RequestPacket($this.passwd)
$this.response = New-ResponsePacket
$this._receiveBuffer = [byte[]]::new([Base]::DEFAULT_BUFFER_SIZE)
$this._InitializeConnection()
}
hidden [void] _InitializeConnection() {
try {
$hostEntry = [System.Net.Dns]::GetHostEntry($this.hostname)
if ($hostEntry.AddressList.Length -eq 0) {
throw [System.Net.Sockets.SocketException]::new([int][System.Net.Sockets.SocketError]::HostNotFound)
}
$ipv4Address = $hostEntry.AddressList | Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | Select-Object -First 1
if (-not $ipv4Address) {
throw [System.InvalidOperationException]::new("No IPv4 address found for hostname: $($this.hostname)")
}
$endpoint = [System.Net.IPEndPoint]::new($ipv4Address, $this.port)
$this._socket = [System.Net.Sockets.Socket]::new(
[System.Net.Sockets.AddressFamily]::InterNetwork,
[System.Net.Sockets.SocketType]::Dgram,
[System.Net.Sockets.ProtocolType]::UDP
)
$this._socket.Connect($endpoint)
$this._socket.ReceiveTimeout = [Base]::DEFAULT_RECEIVE_TIMEOUT
$this._socket.SendTimeout = [Base]::DEFAULT_SEND_TIMEOUT
}
catch [System.Net.Sockets.SocketException] {
$this._Cleanup()
throw [System.InvalidOperationException]::new(
"Failed to create UDP connection to $($this.hostname):$($this.port). Error: $($_.Exception.Message)",
$_.Exception
)
}
catch {
$this._Cleanup()
throw [System.InvalidOperationException]::new(
"Failed to initialize connection to $($this.hostname):$($this.port). Error: $($_.Exception.Message)",
$_.Exception
)
}
}
hidden [void] _ThrowIfDisposed() {
if ($this._disposed) {
throw [System.ObjectDisposedException]::new($this.GetType().Name)
}
}
hidden [bool] _IsConnected() {
return $this._socket -and -not $this._disposed -and $this._socket.Connected
}
[string] ToString () {
$status = if ($this._IsConnected()) { 'Connected' } else { 'Disconnected' }
return 'Rcon connection {0}:{1} ({2})' -f $this.hostname, $this.port, $status
}
[string] _send([string]$msg) {
return $this._send($msg, [Base]::DEFAULT_RECEIVE_TIMEOUT)
}
[string] _send([string]$msg, [int]$timeout) {
if ([string]::IsNullOrEmpty($msg)) {
throw [System.ArgumentException]::new('Message cannot be null or empty', 'msg')
}
if ($timeout -le 0) {
throw [System.ArgumentOutOfRangeException]::new('timeout', 'Timeout must be positive')
}
$this._ThrowIfDisposed()
if (-not $this._IsConnected()) {
throw [System.InvalidOperationException]::new('Socket is not connected')
}
try {
$payload = $this.request.Payload($msg)
$bytesSent = $this._socket.Send($payload)
if ($bytesSent -ne $payload.Length) {
Write-Warning "Not all bytes were sent. Expected: $($payload.Length), Sent: $bytesSent"
}
$responseData = [System.Text.StringBuilder]::new()
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$headerLength = $this.response.Header().Length
do {
try {
$bytesReceived = $this._socket.Receive($this._receiveBuffer)
if ($bytesReceived -gt 0) {
$dataStartIndex = [Math]::Min($headerLength - 1, $bytesReceived)
$responseText = [System.Text.Encoding]::ASCII.GetString($this._receiveBuffer, $dataStartIndex, $bytesReceived - $dataStartIndex)
$responseData.Append($responseText) | Out-Null
}
}
catch [System.Net.Sockets.SocketException] {
if ($_.Exception.SocketErrorCode -eq 'TimedOut') {
Write-Debug 'Socket receive timeout - continuing to wait for more data'
continue
}
else {
throw [System.InvalidOperationException]::new(
"Socket error during receive: $($_.Exception.Message)",
$_.Exception
)
}
}
} while ($sw.ElapsedMilliseconds -lt $timeout)
$sw.Stop()
return $responseData.ToString()
}
catch [System.Net.Sockets.SocketException] {
throw [System.InvalidOperationException]::new(
"Network error during send/receive: $($_.Exception.Message)",
$_.Exception
)
}
}
hidden [void] _Cleanup() {
if ($this._socket) {
try {
if ($this._socket.Connected) {
$this._socket.Shutdown([System.Net.Sockets.SocketShutdown]::Both)
}
}
catch {
Write-Debug "Error during socket shutdown: $($_.Exception.Message)"
}
finally {
$this._socket.Close()
$this._socket = $null
}
}
}
[void] _close() {
$this.Dispose()
}
# Dispose implementation (following IDisposable pattern)
[void] Dispose() {
$this.Dispose($true)
[System.GC]::SuppressFinalize($this)
}
hidden [void] Dispose([bool]$disposing) {
if (-not $this._disposed) {
if ($disposing) {
$this._Cleanup()
}
$this._disposed = $true
}
}
}
function New-Base {
param(
[Parameter(Mandatory)]
[string]$hostname,
[Parameter(Mandatory)]
[ValidateRange(1, 65535)]
[int]$port,
[Parameter(Mandatory)]
[string]$passwd
)
[Base]::new($hostname, $port, $passwd)
}