mirror of
https://github.com/onyx-and-iris/q3rcon.git
synced 2026-03-14 23:09:15 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f552b96737 | |||
| 6d321aa816 | |||
| eeb2c74387 | |||
| c4e12fcaf7 | |||
| cf82b29de5 | |||
| b1161e1e97 | |||
| f74fbedacc | |||
| 3f45588afb | |||
| caffd65cb3 | |||
| 6b33882c0c | |||
| abb33742aa | |||
| 51e8ac85be | |||
| 79d53f34da | |||
| 7c5a3523bf | |||
| 44a528e31d | |||
| 48b23321e5 | |||
| d05ed91473 | |||
| 5c28c4e8b7 | |||
| 53f30981fd | |||
| fce6fa43fc | |||
| 0b9546ee0e | |||
| a55de6fe50 | |||
| e4f3366a67 | |||
| 0fe373d1d1 | |||
| e0668d11d9 | |||
| f3853f6a4d | |||
| c4e2dacee9 | |||
| 3be7ddb36b | |||
| c22b07808f | |||
| c015770c2c | |||
| cd15e89837 | |||
| c3e8013c4f | |||
| 89dd2d2eb1 | |||
| c04301562e | |||
| e25104091d | |||
| c478598112 | |||
|
|
b224f2dc43 | ||
|
|
c4b587ee65 | ||
|
|
8f252951ff | ||
| 35ffa55fb9 | |||
|
|
19f5ec4a76 | ||
|
|
5173f32fde | ||
| 7782e7f8bf | |||
| 09f316e5f4 | |||
| 430e9be1f7 | |||
| a27809643a | |||
| 485978956c | |||
| c51dba5ead | |||
| 0bc19a718b | |||
| adebc61b98 | |||
| 51ff562ac4 | |||
| 313d96fffa |
30
.github/workflows/golang-ci.yml
vendored
Normal file
30
.github/workflows/golang-ci.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['main']
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
pull_request:
|
||||||
|
branches: ['main']
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
timeout-minutes: 3
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.6.0
|
||||||
|
args: --config .golangci.yml
|
||||||
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
-
|
||||||
|
name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
-
|
||||||
|
name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: '~> v2'
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
30
.github/workflows/update-go-modules.yml
vendored
Normal file
30
.github/workflows/update-go-modules.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: Auto-Update Go Modules
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 1" # Runs every Monday at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-go-modules:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Update Dependencies
|
||||||
|
run: |
|
||||||
|
go get -u ./...
|
||||||
|
go mod tidy
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add go.mod go.sum
|
||||||
|
git commit -m "chore: auto-update Go modules"
|
||||||
|
git push
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -24,5 +24,9 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
.envrc
|
||||||
|
|
||||||
cmd/codrcon
|
# Added by goreleaser init:
|
||||||
|
dist/
|
||||||
|
|
||||||
|
cmd/codrcon
|
||||||
|
|||||||
142
.golangci.yml
Normal file
142
.golangci.yml
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 3m
|
||||||
|
tests: true
|
||||||
|
go: '1.24'
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable: [errcheck, errorlint, godot, revive, staticcheck]
|
||||||
|
enable:
|
||||||
|
# Default enabled linters
|
||||||
|
- errcheck # Check for unchecked errors
|
||||||
|
- govet # Go's built-in vetting tool
|
||||||
|
- ineffassign # Detect ineffectual assignments
|
||||||
|
- staticcheck # Advanced static analysis
|
||||||
|
- unused # Check for unused code
|
||||||
|
# Additional useful linters
|
||||||
|
- misspell # Detect common misspellings
|
||||||
|
- unparam # Check for unused function parameters
|
||||||
|
- gosec # Security checks
|
||||||
|
- asciicheck # Check for non-ASCII characters
|
||||||
|
- errname # Check error variable names
|
||||||
|
- godot # Check for missing periods in comments
|
||||||
|
- revive # Highly configurable linter for style and correctness
|
||||||
|
- gocritic # Detect code issues and suggest improvements
|
||||||
|
- gocyclo # Check for cyclomatic complexity
|
||||||
|
- dupl # Check for code duplication
|
||||||
|
- predeclared # Check for shadowing of predeclared identifiers
|
||||||
|
- copyloopvar # Check for loop variable capture in goroutines
|
||||||
|
- errorlint # Check for common mistakes in error handling
|
||||||
|
- goconst # Check for repeated strings that could be constants
|
||||||
|
- gosmopolitan # Check for non-portable code
|
||||||
|
|
||||||
|
settings:
|
||||||
|
misspell:
|
||||||
|
locale: UK
|
||||||
|
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
check-blank: true
|
||||||
|
exclude-functions:
|
||||||
|
- fmt.Fprintf
|
||||||
|
- fmt.Fprintln
|
||||||
|
- fmt.Printf
|
||||||
|
- fmt.Println
|
||||||
|
- fmt.Errorf
|
||||||
|
|
||||||
|
revive:
|
||||||
|
severity: warning
|
||||||
|
rules:
|
||||||
|
# Code quality and style
|
||||||
|
- name: exported
|
||||||
|
arguments:
|
||||||
|
- 'checkPrivateReceivers'
|
||||||
|
- 'sayRepetitiveInsteadOfStutters'
|
||||||
|
- name: var-naming
|
||||||
|
- name: package-comments
|
||||||
|
- name: range-val-in-closure
|
||||||
|
- name: time-naming
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: context-keys-type
|
||||||
|
- name: dot-imports
|
||||||
|
- name: empty-block
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: unexported-return
|
||||||
|
- name: unreachable-code
|
||||||
|
- name: unused-parameter
|
||||||
|
- name: var-declaration
|
||||||
|
- name: blank-imports
|
||||||
|
- name: range
|
||||||
|
|
||||||
|
# Disabled rules (can be enabled if needed)
|
||||||
|
# - name: line-length-limit
|
||||||
|
# arguments: [120]
|
||||||
|
# - name: function-length
|
||||||
|
# arguments: [50, 0]
|
||||||
|
# - name: cyclomatic
|
||||||
|
# arguments: [10]
|
||||||
|
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
- G104 # Duplicated errcheck checks
|
||||||
|
- G115 # integer overflow conversion int -> uint32
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
warn-unused: false
|
||||||
|
rules:
|
||||||
|
# Exclude some linters from running on tests files.
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- gocyclo
|
||||||
|
- errcheck
|
||||||
|
- dupl
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
# Formatters configuration
|
||||||
|
formatters:
|
||||||
|
# Enable specific formatters
|
||||||
|
enable:
|
||||||
|
- gofumpt # Stricter gofmt alternative
|
||||||
|
- goimports # Organizes imports
|
||||||
|
- gci # Controls import order/grouping
|
||||||
|
- golines # Enforces line length
|
||||||
|
|
||||||
|
# Formatter-specific settings
|
||||||
|
settings:
|
||||||
|
goimports:
|
||||||
|
local-prefixes: [github.com/onyx-and-iris/q3rcon]
|
||||||
|
|
||||||
|
gci:
|
||||||
|
# Define import sections order
|
||||||
|
sections:
|
||||||
|
- standard # Standard library
|
||||||
|
- default # Everything else
|
||||||
|
- prefix(github.com/onyx-and-iris/q3rcon) # Current module
|
||||||
|
|
||||||
|
gofumpt:
|
||||||
|
extra-rules: true # Enable additional formatting rules
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
warn-unused: true
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# Limit the number of same issues reported to avoid spam
|
||||||
|
max-same-issues: 50
|
||||||
|
|
||||||
|
# Limit the number of issues per linter to keep output manageable
|
||||||
|
max-issues-per-linter: 100
|
||||||
57
.goreleaser.yaml
Normal file
57
.goreleaser.yaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||||
|
# Make sure to check the documentation at https://goreleaser.com
|
||||||
|
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
# You may remove this if you don't use go modules.
|
||||||
|
- go mod tidy
|
||||||
|
# you may remove this if you don't need go generate
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- main: ./cmd/q3rcon/
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- formats: ['tar.gz']
|
||||||
|
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- title .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
|
# use zip for windows archives
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats: ['zip']
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
- '^chore:'
|
||||||
|
|
||||||
|
release:
|
||||||
|
footer: >-
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||||
50
CHANGELOG.md
50
CHANGELOG.md
@ -11,6 +11,56 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
|
# [0.5.3] - 2026-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Spinner for long running CLI commands.
|
||||||
|
|
||||||
|
# [0.5.1] - 2026-02-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- CLI configuration can be managed through env vars, see [Environment Variables](https://github.com/onyx-and-iris/q3rcon?tab=readme-ov-file#environment-variables) under Configuration in README.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The CLI now supports `--long` and `-short` style flags. Several examples in README.
|
||||||
|
- `--help` output has been improved.
|
||||||
|
- Colour codes have been removed from CLI output. This makes the responses easier to read.
|
||||||
|
|
||||||
|
|
||||||
|
# [0.4.1] - 2026-02-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- macos build to releases
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- exitOnError() removed in favour of a [deferred exit function](https://github.com/onyx-and-iris/q3rcon/blob/cd15e8983726177d6edd985a8bf3d7f4e0d7f346/cmd/q3rcon/main.go#L21), this ensures the closer() cleanup function is always called.
|
||||||
|
- the included CLI now uses a [timeouts map](https://github.com/onyx-and-iris/q3rcon/blob/cd15e8983726177d6edd985a8bf3d7f4e0d7f346/cmd/q3rcon/main.go#L109).
|
||||||
|
- even though this is only an example implementation it should still be basically usable.
|
||||||
|
|
||||||
|
# [0.3.0] - 2025-04-05
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `-loglevel` flag is now of type string. It accepts any one of trace, debug, info, warn, error, fatal or panic.
|
||||||
|
- It defaults to warn.
|
||||||
|
|
||||||
|
# [0.2.0] - 2025-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- If neither the interactive flag is passed or any command line arguments then a *rcon status* command will be run.
|
||||||
|
|
||||||
|
# [0.1.0] - 2024-11-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `-P` flag changed to `-r` for setting rcon password. This is to disambiguate it from the port (-p) flag.
|
||||||
|
|
||||||
# [0.0.3] - 2024-11-24
|
# [0.0.3] - 2024-11-24
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Onyx and Iris
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
80
README.md
80
README.md
@ -17,9 +17,15 @@ Quake3 Rcon works by firing UDP packets to the game server port, responses may b
|
|||||||
|
|
||||||
Rcon itself is insecure and each packet includes the password so I don't suggest using it remotely. If you have direct access to the server then SSH in first, then use this tool locally.
|
Rcon itself is insecure and each packet includes the password so I don't suggest using it remotely. If you have direct access to the server then SSH in first, then use this tool locally.
|
||||||
|
|
||||||
## Use
|
---
|
||||||
|
|
||||||
`go get github.com/onyx-and-iris/q3rcon`
|
## Package
|
||||||
|
|
||||||
|
#### Use
|
||||||
|
|
||||||
|
```console
|
||||||
|
go get github.com/onyx-and-iris/q3rcon
|
||||||
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@ -91,41 +97,91 @@ rcon, err := q3rcon.New(
|
|||||||
q3rcon.WithTimeouts(timeouts))
|
q3rcon.WithTimeouts(timeouts))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Command line
|
## Command line
|
||||||
|
|
||||||
Pass `host`, `port` and `password` as flags, for example:
|
### Install
|
||||||
|
|
||||||
|
```console
|
||||||
|
go install github.com/onyx-and-iris/q3rcon/cmd/q3rcon@latest
|
||||||
```
|
```
|
||||||
q3rcon -h=localhost -p=30000 -P="rconpassword" "mapname"
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
Pass `--host`, `--port` and `--rconpass` as flags, for example:
|
||||||
|
|
||||||
|
```console
|
||||||
|
q3rcon --host=localhost --port=30000 --rconpass="rconpassword" "mapname"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `host` defaults to "localhost"
|
- `host` defaults to "localhost"
|
||||||
- `port` defaults to 28960
|
- `port` defaults to 28960
|
||||||
- `password` defaults to ""
|
- `rconpass` defaults to ""
|
||||||
|
|
||||||
Arguments following the flags will be sent as rcon commands. You may send multiple arguments.
|
Arguments following the flags will be sent as rcon commands. You may send multiple arguments.
|
||||||
|
|
||||||
#### Interactive mode
|
#### Environment Variables
|
||||||
|
|
||||||
|
example .envrc:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export Q3RCON_HOST="localhost"
|
||||||
|
export Q3RCON_PORT=28960
|
||||||
|
export Q3RCON_RCONPASS="rconpassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive mode
|
||||||
|
|
||||||
Pass `interactive (-i shorthand)` flag to enable interactive mode, for example:
|
Pass `interactive (-i shorthand)` flag to enable interactive mode, for example:
|
||||||
|
|
||||||
```
|
```console
|
||||||
q3rcon -h=localhost -p=30000 -P="rconpassword" -i
|
q3rcon -H=localhost -p=30000 -r="rconpassword" -i
|
||||||
```
|
```
|
||||||
|
|
||||||
If interactive mode is enabled, any arguments sent on the command line will be ignored.
|
If interactive mode is enabled, any arguments sent on the command line will be ignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Your own implementation
|
## Your own implementation
|
||||||
|
|
||||||
The included CLI is a generic implementation, while it can be used out of the box you may find that some requests result in fragmented responses. The solution is to implement your own version, adjusting the timings with the functional options as detailed above. I could have increased the default timeouts but that would add unnecessary delay for most requests, so I decided to leave those details to the users of the package.
|
The included CLI is a generic implementation, while it can be used out of the box you may find that some requests result in fragmented responses. The solution is to implement your own version, adjusting the timings with the functional options as detailed above.
|
||||||
|
|
||||||
Since you can include the q3rcon package into your own package you can easily make your own modifications, for example, I added [colour to the terminal][status] and [reformatted some of the responses][mapname].
|
Since you can include the q3rcon package into your own CLI/package you can easily make your own modifications, for example, I added [colour to the terminal][status] and [tabulated some of the responses][mapname].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
Log level may be set by passing the `-l` flag with a number from 0 up to 6 where
|
The `--loglevel` flag allows you to control the verbosity of the application's logging output.
|
||||||
|
|
||||||
|
Acceptable values for this flag are:
|
||||||
|
|
||||||
|
- `trace`
|
||||||
|
- `debug`
|
||||||
|
- `info`
|
||||||
|
- `warn`
|
||||||
|
- `error`
|
||||||
|
- `fatal`
|
||||||
|
- `panic`
|
||||||
|
|
||||||
|
For example, to set the log level to `debug`, you can use:
|
||||||
|
|
||||||
|
```console
|
||||||
|
q3rcon -H=localhost -p=28960 -r="rconpassword" -l=debug -i
|
||||||
|
```
|
||||||
|
|
||||||
|
The default log level is `warn` if the flag is not specified.
|
||||||
|
|
||||||
|
## Further Notes
|
||||||
|
|
||||||
|
This rcon client is fully compatible with the [Q3 Rcon Proxy][q3rcon-proxy] package.
|
||||||
|
|
||||||
0 = Panic, 1 = Fatal, 2 = Error, 3 = Warning, 4 = Info, 5 = Debug, 6 = Trace
|
|
||||||
|
|
||||||
[status]: ./img/status.png
|
[status]: ./img/status.png
|
||||||
[mapname]: ./img/mapname.png
|
[mapname]: ./img/mapname.png
|
||||||
|
[q3rcon-proxy]: https://github.com/onyx-and-iris/q3rcon-proxy/tree/dev
|
||||||
65
Taskfile.yml
Normal file
65
Taskfile.yml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
PROGRAM: q3rcon
|
||||||
|
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
|
||||||
|
BIN_DIR: bin
|
||||||
|
|
||||||
|
WINDOWS: '{{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe'
|
||||||
|
LINUX: '{{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64'
|
||||||
|
MACOS: '{{.BIN_DIR}}/{{.PROGRAM}}_darwin_amd64'
|
||||||
|
VERSION:
|
||||||
|
sh: 'git describe --tags $(git rev-list --tags --max-count=1)'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
desc: Build the q3rcon project
|
||||||
|
cmds:
|
||||||
|
- task: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build the q3rcon project
|
||||||
|
deps: [vet]
|
||||||
|
cmds:
|
||||||
|
- task: build-windows
|
||||||
|
- task: build-linux
|
||||||
|
- task: build-macos
|
||||||
|
|
||||||
|
vet:
|
||||||
|
desc: Vet the code
|
||||||
|
deps: [fmt]
|
||||||
|
cmds:
|
||||||
|
- go vet ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
desc: Fmt the code
|
||||||
|
cmds:
|
||||||
|
- go fmt ./...
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
desc: Build the q3rcon project for Windows
|
||||||
|
cmds:
|
||||||
|
- GOOS=windows GOARCH=amd64 go build -o {{.WINDOWS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
desc: Build the q3rcon project for Linux
|
||||||
|
cmds:
|
||||||
|
- GOOS=linux GOARCH=amd64 go build -o {{.LINUX}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
desc: Build the q3rcon project for macOS
|
||||||
|
cmds:
|
||||||
|
- GOOS=darwin GOARCH=amd64 go build -o {{.MACOS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: Run tests
|
||||||
|
cmds:
|
||||||
|
- go test ./...
|
||||||
|
|
||||||
|
clean:
|
||||||
|
desc: Clean the build artifacts
|
||||||
|
cmds:
|
||||||
|
- '{{.SHELL}} rm -r {{.BIN_DIR}}'
|
||||||
@ -3,108 +3,257 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"regexp"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/chelnak/ysmrr"
|
||||||
|
"github.com/peterbourgon/ff/v4"
|
||||||
|
"github.com/peterbourgon/ff/v4/ffhelp"
|
||||||
|
|
||||||
"github.com/onyx-and-iris/q3rcon"
|
"github.com/onyx-and-iris/q3rcon"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var interactive bool
|
var version string // Version will be set at build time
|
||||||
|
|
||||||
func exit(err error) {
|
type Flags struct {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
Host string
|
||||||
flag.Usage()
|
Port int
|
||||||
os.Exit(1)
|
Rconpass string
|
||||||
|
Interactive bool
|
||||||
|
LogLevel string
|
||||||
|
Version bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f Flags) Validate() error {
|
||||||
|
if f.Port < 1024 || f.Port > 65535 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid port value, got: (%d) expected: in range 1024-65535",
|
||||||
|
f.Port,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(f.Rconpass) < 8 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"invalid rcon password, got: (%s) expected: at least 8 characters",
|
||||||
|
f.Rconpass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var reColourCodes = regexp.MustCompile(`\^[0-9]`)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var exitCode int
|
||||||
host string
|
|
||||||
port int
|
|
||||||
password string
|
|
||||||
loglevel int
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.StringVar(&host, "host", "localhost", "hostname of the server")
|
// Defer exit with the final exit code
|
||||||
flag.StringVar(&host, "h", "localhost", "hostname of the server (shorthand)")
|
defer func() {
|
||||||
flag.IntVar(&port, "port", 28960, "port of the server")
|
if exitCode != 0 {
|
||||||
flag.IntVar(&port, "p", 28960, "port of the server (shorthand)")
|
os.Exit(exitCode)
|
||||||
flag.StringVar(&password, "password", "", "rcon password")
|
}
|
||||||
flag.StringVar(&password, "P", "", "rcon password (shorthand)")
|
}()
|
||||||
|
|
||||||
flag.BoolVar(&interactive, "interactive", false, "run in interactive mode")
|
closer, err := run()
|
||||||
flag.BoolVar(&interactive, "i", false, "run in interactive mode")
|
if closer != nil {
|
||||||
|
defer closer()
|
||||||
flag.IntVar(&loglevel, "loglevel", int(log.WarnLevel), "log level")
|
|
||||||
flag.IntVar(&loglevel, "l", int(log.WarnLevel), "log level (shorthand)")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if slices.Contains(log.AllLevels, log.Level(loglevel)) {
|
|
||||||
log.SetLevel(log.Level(loglevel))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rcon, err := connectRcon(host, port, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Error(err)
|
||||||
}
|
exitCode = 1
|
||||||
defer rcon.Close()
|
|
||||||
|
|
||||||
if interactive {
|
|
||||||
fmt.Printf("Enter 'Q' to exit.\n>> ")
|
|
||||||
err := interactiveMode(rcon, os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(flag.Args()) == 0 {
|
|
||||||
err = errors.New("no rcon commands passed")
|
|
||||||
exit(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arg := range flag.Args() {
|
|
||||||
resp, err := rcon.Send(arg)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Print(resp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, error) {
|
type context struct {
|
||||||
rcon, err := q3rcon.New(host, port, password)
|
client *q3rcon.Rcon
|
||||||
if err != nil {
|
timeouts map[string]time.Duration
|
||||||
|
in io.Reader
|
||||||
|
sm ysmrr.SpinnerManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes the main logic of the application and returns a cleanup function and an error if any.
|
||||||
|
func run() (func(), error) {
|
||||||
|
var flags Flags
|
||||||
|
|
||||||
|
fs := ff.NewFlagSet("q3rcon - A command-line RCON client for Q3 Rcon compatible game servers")
|
||||||
|
fs.StringVar(&flags.Host, 'H', "host", "localhost", "hostname of the gameserver")
|
||||||
|
fs.IntVar(
|
||||||
|
&flags.Port,
|
||||||
|
'p',
|
||||||
|
"port",
|
||||||
|
28960,
|
||||||
|
"port on which the gameserver resides, default is 28960",
|
||||||
|
)
|
||||||
|
fs.StringVar(
|
||||||
|
&flags.Rconpass,
|
||||||
|
'r',
|
||||||
|
"rconpass",
|
||||||
|
"",
|
||||||
|
"rcon password of the gameserver",
|
||||||
|
)
|
||||||
|
|
||||||
|
fs.BoolVar(&flags.Interactive, 'i', "interactive", "run in interactive mode")
|
||||||
|
fs.StringVar(
|
||||||
|
&flags.LogLevel,
|
||||||
|
'l',
|
||||||
|
"loglevel",
|
||||||
|
"info",
|
||||||
|
"Log level (debug, info, warn, error, fatal, panic)",
|
||||||
|
)
|
||||||
|
fs.BoolVar(&flags.Version, 'v', "version", "print version information and exit")
|
||||||
|
|
||||||
|
err := ff.Parse(fs, os.Args[1:],
|
||||||
|
ff.WithEnvVarPrefix("Q3RCON"),
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ff.ErrHelp):
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Flags(fs, "q3rcon [flags] <rcon commands>"))
|
||||||
|
return nil, nil
|
||||||
|
case err != nil:
|
||||||
|
return nil, fmt.Errorf("failed to parse flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.Version {
|
||||||
|
fmt.Printf("q3rcon version: %s\n", versionFromBuild())
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := flags.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return rcon, nil
|
|
||||||
|
level, err := log.ParseLevel(flags.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid log level: %s", flags.LogLevel)
|
||||||
|
}
|
||||||
|
log.SetLevel(level)
|
||||||
|
|
||||||
|
timeouts := map[string]time.Duration{
|
||||||
|
"map": time.Second,
|
||||||
|
"map_rotate": time.Second,
|
||||||
|
"map_restart": time.Second,
|
||||||
|
}
|
||||||
|
log.Debugf("using timeouts: %v", timeouts)
|
||||||
|
|
||||||
|
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass, timeouts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm := ysmrr.NewSpinnerManager()
|
||||||
|
sm.AddSpinner("")
|
||||||
|
|
||||||
|
ctx := &context{
|
||||||
|
client: client,
|
||||||
|
timeouts: timeouts,
|
||||||
|
in: os.Stdin,
|
||||||
|
sm: sm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.Interactive {
|
||||||
|
fmt.Printf("Enter 'Q' to exit.\n>> ")
|
||||||
|
err := interactiveMode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return closer, fmt.Errorf("interactive mode error: %w", err)
|
||||||
|
}
|
||||||
|
return closer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commands := fs.GetArgs()
|
||||||
|
if len(commands) == 0 {
|
||||||
|
log.Debug("no commands provided, defaulting to 'status'")
|
||||||
|
commands = append(commands, "status")
|
||||||
|
}
|
||||||
|
runCommands(ctx, commands)
|
||||||
|
|
||||||
|
return closer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionFromBuild retrieves the version information from the build metadata.
|
||||||
|
func versionFromBuild() string {
|
||||||
|
if version != "" {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return "(unable to read version)"
|
||||||
|
}
|
||||||
|
return strings.Split(info.Main.Version, "-")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectRcon(
|
||||||
|
host string,
|
||||||
|
port int,
|
||||||
|
password string,
|
||||||
|
timeouts map[string]time.Duration,
|
||||||
|
) (*q3rcon.Rcon, func(), error) {
|
||||||
|
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(timeouts))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
closer := func() {
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, closer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// interactiveMode continuously reads from input until a quit signal is given.
|
// interactiveMode continuously reads from input until a quit signal is given.
|
||||||
func interactiveMode(rcon *q3rcon.Rcon, input io.Reader) error {
|
func interactiveMode(ctx *context) error {
|
||||||
scanner := bufio.NewScanner(input)
|
scanner := bufio.NewScanner(ctx.in)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
cmd := scanner.Text()
|
cmd := scanner.Text()
|
||||||
if strings.ToUpper(cmd) == "Q" {
|
if strings.EqualFold(cmd, "Q") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rcon.Send(cmd)
|
if err := runCommand(ctx, cmd); err != nil {
|
||||||
if err != nil {
|
fmt.Printf("Error: %v\n", err)
|
||||||
log.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
fmt.Printf("%s>> ", resp)
|
fmt.Printf(">> ")
|
||||||
}
|
}
|
||||||
|
|
||||||
if scanner.Err() != nil {
|
if scanner.Err() != nil {
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runCommands executes a list of commands sequentially and prints any errors encountered.
|
||||||
|
func runCommands(ctx *context, commands []string) {
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if err := runCommand(ctx, cmd); err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand sends a command to the RCON client and prints the response.
|
||||||
|
// If the command is in the timeouts map, it starts a spinner until the response is received.
|
||||||
|
func runCommand(ctx *context, cmd string) error {
|
||||||
|
before, _, _ := strings.Cut(cmd, " ")
|
||||||
|
_, ok := ctx.timeouts[before]
|
||||||
|
if ok {
|
||||||
|
ctx.sm.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ctx.client.Send(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run command '%s': %w", cmd, err)
|
||||||
|
}
|
||||||
|
if ctx.sm.Running() {
|
||||||
|
ctx.sm.Stop()
|
||||||
|
}
|
||||||
|
fmt.Print(reColourCodes.ReplaceAllString(resp, ""))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,34 +1,33 @@
|
|||||||
package conn
|
package q3rcon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/charmbracelet/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPConn struct {
|
type UDPConn struct {
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(host string, port int) (UDPConn, error) {
|
func newUDPConn(host string, port int) (*UDPConn, error) {
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
|
udpAddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UDPConn{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UDPConn{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Infof("Outgoing address %s", conn.RemoteAddr())
|
log.Infof("Outgoing address %s", conn.RemoteAddr())
|
||||||
|
|
||||||
return UDPConn{
|
return &UDPConn{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Write(buf []byte) (int, error) {
|
func (c *UDPConn) Write(buf []byte) (int, error) {
|
||||||
n, err := c.conn.Write(buf)
|
n, err := c.conn.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -37,8 +36,7 @@ func (c UDPConn) Write(buf []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
|
func (c *UDPConn) Read(buf []byte) (int, error) {
|
||||||
c.conn.SetReadDeadline(timeout)
|
|
||||||
rlen, _, err := c.conn.ReadFromUDP(buf)
|
rlen, _, err := c.conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -46,7 +44,7 @@ func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
|
|||||||
return rlen, nil
|
return rlen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Close() error {
|
func (c *UDPConn) Close() error {
|
||||||
err := c.conn.Close()
|
err := c.conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
26
go.mod
26
go.mod
@ -1,14 +1,30 @@
|
|||||||
module github.com/onyx-and-iris/q3rcon
|
module github.com/onyx-and-iris/q3rcon
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fatih/color v1.18.0
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/chelnak/ysmrr v0.6.0
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/term v0.40.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
63
go.sum
63
go.sum
@ -1,25 +1,56 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/chelnak/ysmrr v0.6.0 h1:kMhO0oI02tl/9szvxrOE0yeImtrK4KQhER0oXu1K/iM=
|
||||||
|
github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc=
|
||||||
|
github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
BIN
img/mapname.png
BIN
img/mapname.png
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
img/status.png
BIN
img/status.png
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 10 KiB |
@ -1,33 +0,0 @@
|
|||||||
package packet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const bufSz = 512
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
magic []byte
|
|
||||||
password string
|
|
||||||
buf *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRequest(password string) Request {
|
|
||||||
return Request{
|
|
||||||
magic: []byte{'\xff', '\xff', '\xff', '\xff'},
|
|
||||||
password: password,
|
|
||||||
buf: bytes.NewBuffer(make([]byte, bufSz)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) Header() []byte {
|
|
||||||
return append(r.magic, []byte("rcon")...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) Encode(cmd string) []byte {
|
|
||||||
r.buf.Reset()
|
|
||||||
r.buf.Write(r.Header())
|
|
||||||
r.buf.WriteString(fmt.Sprintf(" %s %s", r.password, cmd))
|
|
||||||
return r.buf.Bytes()
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package packet
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
magic []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponse() Response {
|
|
||||||
return Response{magic: []byte{'\xff', '\xff', '\xff', '\xff'}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Response) Header() []byte {
|
|
||||||
return append(r.magic, []byte("print\n")...)
|
|
||||||
}
|
|
||||||
21
makefile
21
makefile
@ -1,34 +1,39 @@
|
|||||||
program = q3rcon
|
PROGRAM = q3rcon
|
||||||
|
|
||||||
GO = @go
|
GO = @go
|
||||||
BIN_DIR := bin
|
BIN_DIR := bin
|
||||||
|
|
||||||
WINDOWS=$(BIN_DIR)/$(program)_windows_amd64.exe
|
WINDOWS=$(BIN_DIR)/$(PROGRAM)_windows_amd64.exe
|
||||||
LINUX=$(BIN_DIR)/$(program)_linux_amd64
|
LINUX=$(BIN_DIR)/$(PROGRAM)_linux_amd64
|
||||||
VERSION=$(shell git describe --tags --always --long --dirty)
|
MACOS=$(BIN_DIR)/$(PROGRAM)_darwin_amd64
|
||||||
|
VERSION=$(shell git describe --tags $(shell git rev-list --tags --max-count=1))
|
||||||
|
|
||||||
.DEFAULT_GOAL := build
|
.DEFAULT_GOAL := build
|
||||||
|
|
||||||
.PHONY: fmt vet build windows linux test clean
|
.PHONY: fmt vet build windows linux macos test clean
|
||||||
fmt:
|
fmt:
|
||||||
$(GO) fmt ./...
|
$(GO) fmt ./...
|
||||||
|
|
||||||
vet: fmt
|
vet: fmt
|
||||||
$(GO) vet ./...
|
$(GO) vet ./...
|
||||||
|
|
||||||
build: vet windows linux | $(BIN_DIR)
|
build: vet windows linux macos | $(BIN_DIR)
|
||||||
@echo version: $(VERSION)
|
@echo version: $(VERSION)
|
||||||
|
|
||||||
windows: $(WINDOWS)
|
windows: $(WINDOWS)
|
||||||
|
|
||||||
linux: $(LINUX)
|
linux: $(LINUX)
|
||||||
|
|
||||||
|
macos: $(MACOS)
|
||||||
|
|
||||||
$(WINDOWS):
|
$(WINDOWS):
|
||||||
env GOOS=windows GOARCH=amd64 go build -v -o $(WINDOWS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/q3rcon/
|
env GOOS=windows GOARCH=amd64 go build -v -o $(WINDOWS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
|
||||||
|
|
||||||
$(LINUX):
|
$(LINUX):
|
||||||
env GOOS=linux GOARCH=amd64 go build -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/q3rcon/
|
env GOOS=linux GOARCH=amd64 go build -v -o $(LINUX) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
|
||||||
|
|
||||||
|
$(MACOS):
|
||||||
|
env GOOS=darwin GOARCH=amd64 go build -v -o $(MACOS) -ldflags="-s -w -X main.version=$(VERSION)" ./cmd/$(PROGRAM)/
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test ./...
|
$(GO) test ./...
|
||||||
|
|||||||
66
q3rcon.go
66
q3rcon.go
@ -1,24 +1,31 @@
|
|||||||
package q3rcon
|
package q3rcon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/charmbracelet/log"
|
||||||
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/conn"
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/packet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const respBufSiz = 2048
|
const respBufSiz = 2048
|
||||||
|
|
||||||
|
type encoder interface {
|
||||||
|
encode(cmd string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type decoder interface {
|
||||||
|
isValid(buf []byte) bool
|
||||||
|
decode(buf []byte) string
|
||||||
|
}
|
||||||
|
|
||||||
type Rcon struct {
|
type Rcon struct {
|
||||||
conn conn.UDPConn
|
conn io.ReadWriteCloser
|
||||||
request packet.Request
|
request encoder
|
||||||
response packet.Response
|
response decoder
|
||||||
|
|
||||||
loginTimeout time.Duration
|
loginTimeout time.Duration
|
||||||
defaultTimeout time.Duration
|
defaultTimeout time.Duration
|
||||||
@ -30,15 +37,15 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
|
|||||||
return nil, errors.New("no password provided")
|
return nil, errors.New("no password provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := conn.New(host, port)
|
conn, err := newUDPConn(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error creating UDP connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &Rcon{
|
r := &Rcon{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
request: packet.NewRequest(password),
|
request: newRequest(password),
|
||||||
response: packet.NewResponse(),
|
response: newResponse(),
|
||||||
|
|
||||||
loginTimeout: 5 * time.Second,
|
loginTimeout: 5 * time.Second,
|
||||||
defaultTimeout: 20 * time.Millisecond,
|
defaultTimeout: 20 * time.Millisecond,
|
||||||
@ -50,7 +57,7 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = r.login(); err != nil {
|
if err = r.login(); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error logging in: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
@ -65,7 +72,7 @@ func (r Rcon) login() error {
|
|||||||
default:
|
default:
|
||||||
resp, err := r.Send("login")
|
resp, err := r.Send("login")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("error sending login command: %w", err)
|
||||||
}
|
}
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
continue
|
continue
|
||||||
@ -94,9 +101,14 @@ func (r Rcon) Send(cmdWithArgs string) (string, error) {
|
|||||||
|
|
||||||
go r.listen(timeout, respChan, errChan)
|
go r.listen(timeout, respChan, errChan)
|
||||||
|
|
||||||
_, err := r.conn.Write(r.request.Encode(cmdWithArgs))
|
encodedCmd, err := r.request.encode(cmdWithArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("error encoding command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.conn.Write(encodedCmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error writing command to connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -118,7 +130,17 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
|
|||||||
respChan <- sb.String()
|
respChan <- sb.String()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
rlen, err := r.conn.ReadUntil(time.Now().Add(timeout), respBuf)
|
c, ok := r.conn.(*UDPConn)
|
||||||
|
if !ok {
|
||||||
|
errChan <- errors.New("connection is not a UDPConn")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := c.conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("error setting read deadline: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rlen, err := r.conn.Read(respBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e, ok := err.(net.Error)
|
e, ok := err.(net.Error)
|
||||||
if ok {
|
if ok {
|
||||||
@ -131,15 +153,13 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rlen > len(r.response.Header()) {
|
if r.response.isValid(respBuf[:rlen]) {
|
||||||
if bytes.HasPrefix(respBuf, r.response.Header()) {
|
sb.WriteString(r.response.decode(respBuf[:rlen]))
|
||||||
sb.Write(respBuf[len(r.response.Header()):rlen])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Rcon) Close() {
|
func (r Rcon) Close() error {
|
||||||
r.conn.Close()
|
return r.conn.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
35
request.go
Normal file
35
request.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package q3rcon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bufSz = 1024
|
||||||
|
requestHeader = "\xff\xff\xff\xffrcon"
|
||||||
|
)
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
password string
|
||||||
|
buf *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(password string) request {
|
||||||
|
return request{
|
||||||
|
password: password,
|
||||||
|
buf: bytes.NewBuffer(make([]byte, 0, bufSz)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r request) encode(cmd string) ([]byte, error) {
|
||||||
|
if cmd == "" {
|
||||||
|
return nil, errors.New("command cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.buf.Reset()
|
||||||
|
r.buf.WriteString(requestHeader)
|
||||||
|
r.buf.WriteString(fmt.Sprintf(" %s %s", r.password, cmd))
|
||||||
|
return r.buf.Bytes(), nil
|
||||||
|
}
|
||||||
21
response.go
Normal file
21
response.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package q3rcon
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
const (
|
||||||
|
responseHeader = "\xff\xff\xff\xffprint\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type response struct{}
|
||||||
|
|
||||||
|
func newResponse() response {
|
||||||
|
return response{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) isValid(buf []byte) bool {
|
||||||
|
return len(buf) > len(responseHeader) && bytes.HasPrefix(buf, []byte(responseHeader))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r response) decode(buf []byte) string {
|
||||||
|
return string(buf[len(responseHeader):])
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user