mirror of
https://github.com/onyx-and-iris/q3rcon-ps.git
synced 2025-01-18 16:50:52 +00:00
initial commit
This commit is contained in:
commit
70f76797ea
109
.gitignore
vendored
Normal file
109
.gitignore
vendored
Normal 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
37
README.md
Normal 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
39
examples/cli.ps1
Normal 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
26
examples/commands.ps1
Normal 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
79
lib/Q3Rcon.psm1
Normal 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
88
lib/base.ps1
Normal 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
37
lib/packet.ps1
Normal 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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user