initial commit

This commit is contained in:
onyx-and-iris 2023-11-29 16:18:03 +00:00
commit 70f76797ea
7 changed files with 415 additions and 0 deletions

109
.gitignore vendored Normal file
View File

@ -0,0 +1,109 @@
bin/
obj/
.ionide/
project.lock.json
*-tests.xml
/debug/
/staging/
/Packages/
*.nuget.props
# dotnet cli install/uninstall scripts
dotnet-install.ps1
dotnet-install.sh
dotnet-uninstall-pkgs.sh
dotnet-uninstall-debian-packages.sh
# VS auto-generated solution files for project.json solutions
*.xproj
*.xproj.user
*.suo
# VS auto-generated files for csproj files
*.csproj.user
# Visual Studio IDE directory
.vs/
# VSCode directories that are not at the repository root
/**/.vscode/
# Project Rider IDE files
.idea.powershell/
# Ignore executables
*.exe
*.msi
*.appx
*.msix
# Ignore binaries and symbols
*.pdb
*.dll
*.wixpdb
# Ignore packages
*.deb
*.tar.gz
*.zip
*.rpm
*.pkg
*.nupkg
*.AppImage
# default location for produced nuget packages
/nuget-artifacts
# resgen output
gen
# Per repo profile
.profile.ps1
# macOS
.DS_Store
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.AppleDouble
.LSOverride
# TestsResults
TestsResults*.xml
ParallelXUnitResults.xml
xUnitResults.xml
# Resharper settings
PowerShell.sln.DotSettings.user
*.msp
StyleCop.Cache
# Ignore SelfSignedCertificate autogenerated files
test/tools/Modules/SelfSignedCertificate/
# BenchmarkDotNet artifacts
test/perf/BenchmarkDotNet.Artifacts/
# Test generated module
test/tools/Modules/Microsoft.PowerShell.NamedPipeConnection/
# Test generated startup profile
StartupProfileData-NonInteractive
# Ignore logfiles
logfile/*
# Manifest
*.psd1
# config
config.psd1

37
README.md Normal file
View File

@ -0,0 +1,37 @@
# Q3 Rcon client for Powershell
Send Rcon commands to your Quake-3 Engine server from Powershell!
## Tested against
Currently only tested on COD servers (2, 4 and 5).
## Requirements
- Powershell 7.2+
## Installation
`Install-Module -Name Q3Rcon -Scope CurrentUser`
## Use
```powershell
Import-Module Q3Rcon
try {
$rcon = Connect-Rcon -hostname "hostname.server" -port 28960 -passwd "strongrconpassword"
$rcon.Map()
"Rotating the map..."
$rcon.MapRotate()
Start-Sleep -Milliseconds 3000 # wait for map to rotate
$rcon.Map()
}
finally {
Disconnect-Rcon -rcon $rcon
}
```

39
examples/cli.ps1 Normal file
View File

@ -0,0 +1,39 @@
[cmdletbinding()]
param()
Import-Module ../lib/Q3Rcon.psm1
Function Read-HostUntilEmpty {
param([object]$rcon)
"Input 'Q' or <Enter> to exit."
while (($line = Read-Host -Prompt "Send command") -cne [string]::Empty) {
if ($line -eq "Q") {
break
}
if ($line -in @("fast_restart", "map_rotate", "map_restart")) {
$cmd = $line -replace '(?:^|_)(\p{L})', { $_.Groups[1].Value.ToUpper() }
$resp = $rcon.$cmd()
}
else {
$resp = $rcon.Send($line)
}
$resp | Write-Host
}
}
Function Get-ConnFromPSD1 {
$configpath = Join-Path $PSScriptRoot "config.psd1"
return Import-PowerShellDataFile -Path $configpath
}
try {
$conn = Get-ConnFromPSD1
$rcon = Connect-Rcon -hostname $conn.host -port $conn.port -passwd $conn.passwd
Read-HostUntilEmpty -rcon $rcon
}
finally {
Disconnect-Rcon -rcon $rcon
}

26
examples/commands.ps1 Normal file
View File

@ -0,0 +1,26 @@
[cmdletbinding()]
param()
Import-Module ../lib/Q3Rcon.psm1
Function Get-ConnFromPSD1 {
$configpath = Join-Path $PSScriptRoot "config.psd1"
return Import-PowerShellDataFile -Path $configpath
}
try {
$conn = Get-ConnFromPSD1
$rcon = Connect-Rcon -hostname $conn.host -port $conn.port -passwd $conn.passwd
$rcon.Map()
"Rotating the map..."
$rcon.MapRotate()
Start-Sleep -Milliseconds 3000 # wait for map to rotate
$rcon.Map()
}
finally {
Disconnect-Rcon -rcon $rcon
}

79
lib/Q3Rcon.psm1 Normal file
View File

@ -0,0 +1,79 @@
. $PSScriptRoot\packet.ps1
. $PSScriptRoot\base.ps1
class Rcon {
[Object]$base
Rcon ([string]$hostname, [int]$port, [string]$passwd) {
$this.base = New-Base -hostname $hostname -port $port -passwd $passwd
}
[Rcon] Login() {
$resp = $this.base._send("login")
if ($resp -in @("Bad rcon", "Bad rconpassword.", "Invalid password.")) {
throw "invalid rcon password"
}
$this.base.ToString() | Write-Debug
return $this
}
[string] Send($msg) {
return $this.base._send($msg)
}
[void] Say($msg) {
$this.base._send($msg)
}
[void] FastRestart() {
$this.base._send("fast_restart", 2000)
}
[void] MapRotate() {
$this.base._send("map_rotate", 2000)
}
[void] MapRestart() {
$this.base._send("map_restart", 2000)
}
[string] Map() {
return $this.base._send("mapname")
}
[void] SetMap($mapname) {
$this.base._send("map mp_$mapname")
}
[string] Gametype() {
return $this.base._send("g_gametype")
}
[void] SetGametype($gametype) {
$this.base._send("g_gametype $gametype")
}
[string] HostName() {
return $this.base._send("sv_hostname")
}
[void] SetHostName($hostname) {
$this.base._send("sv_hostname $hostname")
}
}
Function Connect-Rcon {
param([string]$hostname, [int]$port, [string]$passwd)
[Rcon]::new($hostname, $port, $passwd).Login()
}
Function Disconnect-Rcon {
param([Rcon]$rcon)
$rcon.base._close()
"Disconnected from {0}:{1}" -f $rcon.base.hostname, $rcon.base.port | Write-Debug
}
Export-ModuleMember -Function Connect-Rcon, Disconnect-Rcon

88
lib/base.ps1 Normal file
View File

@ -0,0 +1,88 @@
class Base {
[string]$hostname
[int]$port
[string]$passwd
[Object]$request
[Object]$response
hidden [System.Net.Sockets.Socket] $_socket
Base ([string]$hostname, [int]$port, [string]$passwd) {
$this.hostname = $hostname
$this.port = $port
$this.passwd = $passwd
$this.request = New-RequestPacket($this.passwd)
$this.response = New-ResponsePacket
$ip = [system.net.IPAddress]::Parse([System.Net.Dns]::GetHostAddresses($this.hostname)[0].IPAddressToString)
$endpoint = New-Object System.Net.IPEndPoint $ip, $this.port
try {
$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 = 100
}
catch [System.Net.Sockets.SocketException] {
throw "Failed to create UDP connection to server."
}
}
[string] ToString () {
return "Rcon connection {0}:{1} with pass {2}" -f $this.hostname, $this.port, $this.passwd
}
[string] _send([string]$msg) {
$this._socket.Send($this.request.Payload($msg))
[string[]]$data = @()
$sw = [Diagnostics.Stopwatch]::StartNew()
While ($sw.ElapsedMilliseconds -lt 50) {
try {
$buf = New-Object System.Byte[] 4096
$this._socket.Receive($buf)
$data += [System.Text.Encoding]::ASCII.GetString($($buf | Select-Object -Skip $($this.response.Header().Length - 1)))
}
catch [System.Net.Sockets.SocketException] {
if ( $_.Exception.SocketErrorCode -eq 'TimedOut' ) {
"finished waiting for fragment" | Write-Debug
}
}
}
$sw.Stop()
return [string]::Join("", $data)
}
[string] _send([string]$msg, [int]$timeout) {
$this._socket.Send($this.request.Payload($msg))
[string[]]$data = @()
$sw = [Diagnostics.Stopwatch]::StartNew()
While ($sw.ElapsedMilliseconds -lt $timeout) {
try {
$buf = New-Object System.Byte[] 4096
$this._socket.Receive($buf)
$data += [System.Text.Encoding]::ASCII.GetString($($buf | Select-Object -Skip $($this.response.Header().Length - 1)))
}
catch [System.Net.Sockets.SocketException] {
if ( $_.Exception.SocketErrorCode -eq 'TimedOut' ) {
"finished waiting for fragment" | Write-Debug
}
}
}
return [string]::Join("", $data)
}
[void] _close() {
$this._socket.Close()
}
}
Function New-Base {
param([string]$hostname, [int]$port, [string]$passwd)
[Base]::new($hostname, $port, $passwd)
}

37
lib/packet.ps1 Normal file
View File

@ -0,0 +1,37 @@
class Packet {
[System.Byte[]]$MAGIC = @(, 0xFF * 4)
[string] Header() {
throw "method not implemented"
}
}
class RequestPacket : Packet {
[string]$passwd
RequestPacket([string]$passwd) {
$this.passwd = $passwd
}
[System.Byte[]] Header() {
return $this.MAGIC + [System.Text.Encoding]::ASCII.GetBytes("rcon")
}
[System.Byte[]] Payload([string]$msg) {
return $this.Header() + [System.Text.Encoding]::ASCII.GetBytes($(" {0} {1}" -f $this.passwd, $msg))
}
}
class ResponsePacket : Packet {
[System.Byte[]] Header() {
return $this.MAGIC + [System.Text.Encoding]::ASCII.GetBytes("print\n")
}
}
Function New-RequestPacket([string]$passwd) {
[RequestPacket]::new($passwd)
}
Function New-ResponsePacket {
[ResponsePacket]::new()
}