├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── build │ └── main.go └── csgo-rcon │ ├── .gitignore │ ├── command.example.cfg │ ├── config.example.json │ └── main.go ├── go.mod ├── go.sum └── rcon.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "00:00" 8 | timezone: Etc/UTC 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | go-version: "1.17" 10 | 11 | jobs: 12 | release: 13 | name: Build & Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ env.go-version }} 24 | - name: Build 25 | run: | 26 | go run ./cmd/build -all 27 | - name: Extract Metadata 28 | id: extract 29 | uses: forewing/git-metadata@v1 30 | 31 | - name: Release 32 | uses: softprops/action-gh-release@v1 33 | with: 34 | body: ${{ steps.extract.outputs.changes-formatted }} 35 | name: Release ${{ steps.extract.outputs.tag-current }} 36 | files: | 37 | output/*.tar.gz 38 | output/*.zip 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Build output 15 | 16 | /output 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Forewing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csgo-rcon 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/forewing/csgo-rcon?style=flat-square)](https://goreportcard.com/report/github.com/forewing/csgo-rcon) 4 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/forewing/csgo-rcon?style=flat-square)](https://github.com/forewing/csgo-rcon/releases/latest) 5 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/forewing/csgo-rcon)](https://pkg.go.dev/github.com/forewing/csgo-rcon) 6 | 7 | Golang package for CS:GO RCON Protocol client. Also support other games using the protocol (Source Engine games, Minecraft, etc.) 8 | 9 | > For the protocol specification, go to [Source RCON Protocol from Valve](http://developer.valvesoftware.com/wiki/Source_RCON_Protocol) 10 | 11 | > Need a web-based admin panel? Check it out at [forewing/webrcon-server](https://github.com/forewing/webrcon-server)! 12 | 13 | ## Usage 14 | 15 | 1. Import the package 16 | 17 | ```go 18 | import "github.com/forewing/csgo-rcon" 19 | ``` 20 | 21 | 2. Create a client with `rcon.New(address, password string, timeout time.Duration)`, assuming your server rcon are hosted at `10.114.51.41:27015`, with password `password`, and you want the connection timeout to be 2 seconds. 22 | 23 | ```go 24 | c := rcon.New("10.114.51.41:27015", "password", time.Second * 2) 25 | ``` 26 | 27 | 3. Execute commands use `*Client.Execute(cmd string)`. Execute once if no "\n" provided. Return result message and nil on success, empty string and an error on failure. 28 | 29 | ```go 30 | // Execute a single command 31 | msg, err := c.Execute("bot_add_ct") 32 | 33 | // Execute multiple commands at once 34 | // Source engine games treat `;` as command separator 35 | // May not work in other games, test before use 36 | msg, err := c.Execute("game_mode 1; game_type 0; changelevel de_dust2") 37 | ``` 38 | 39 | 4. Note: If `cmd` includes "\n", it is treated as a script file. Splitted and trimmed into lines. Line starts with "//" will be treated as comment and ignored. When all commands seccess, concatted messages and nil will be returned. Once failed, concatted previous succeeded messages and an error will be returned. 40 | 41 | ```go 42 | cmd := `game_mode 1 43 | game_type 0 44 | // run_game half_life_3 (ignored) 45 | changelevel de_dust2` 46 | 47 | // Execute multiple commands separately 48 | msg, err := c.Execute(cmd) 49 | ``` 50 | 51 | 52 | ## Command Line Tool 53 | 54 | ### Install 55 | 56 | ``` 57 | go get -u github.com/forewing/csgo-rcon/cmd/csgo-rcon 58 | ``` 59 | 60 | Or download from [release page](https://github.com/forewing/csgo-rcon/releases/latest). 61 | 62 | ### Usage 63 | 64 | ``` 65 | Usage of csgo-rcon: 66 | -a address 67 | address of the server RCON, in the format of HOST:PORT. (default "127.0.0.1:27015") 68 | -c file 69 | load configs from file instead of flags. 70 | -f file 71 | read commands from file, "-" for stdin. From arguments if not set. 72 | -i interact with the console. 73 | -m file 74 | read completions from file 75 | -p password 76 | password of the RCON. 77 | -t timeout 78 | timeout of the connection (seconds). (default 1) 79 | ``` 80 | 81 | 1. From arguments 82 | 83 | ``` 84 | $ csgo-rcon -c config.json mp_warmuptime 999 85 | L **/**/20** - **:**:**: rcon from "**.**.**.**:***": command "mp_warmuptime 999" 86 | ``` 87 | 88 | 2. From file (`-` for stdin) 89 | 90 | ``` 91 | $ csgo-rcon -c config.json -f commands.cfg 92 | ``` 93 | 94 | 3. Interactive 95 | 96 | ``` 97 | $ csgo-rcon -c config.json -i 98 | >>> bot_add_ct 99 | L **/**/20** - **:**:**: "Derek<4><>" connected, address "" 100 | L **/**/20** - **:**:**: "Derek<4>" switched from team to 101 | L **/**/20** - **:**:**: "Derek<4><>" entered the game 102 | L **/**/20** - **:**:**: rcon from "**.**.**.**:***": command "bot_add_ct" 103 | >>> users 104 | 105 | 0 users 106 | L **/**/20** - **:**:**: rcon from "**.**.**.**:***": command "users" 107 | >>> ^C 108 | ``` 109 | 110 | 4 .Completion 111 | 112 | ``` sh 113 | # First download the completion file from your server 114 | csgo-rcon -c config.json cvarlist > cmds.txt 115 | # and remove top 2 and bottom 2 lines 116 | tail -n +3 cmds.txt | head -n -2 > cmds.txt.bak && mv cmds.txt.bak cmds.txt 117 | # then use -m flag to specify the completion file 118 | csgo-rcon -c config.json -i -m cmds.txt 119 | ``` 120 | 121 | -------------------------------------------------------------------------------- /cmd/build/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/forewing/gobuild" 8 | ) 9 | 10 | const ( 11 | name = "csgo-rcon" 12 | ) 13 | 14 | var ( 15 | flagAll = flag.Bool("all", false, "build for all platforms") 16 | 17 | target = gobuild.Target{ 18 | Source: "./cmd/csgo-rcon", 19 | OutputName: name, 20 | OutputPath: "./output", 21 | CleanOutput: true, 22 | 23 | ExtraFlags: []string{"-trimpath"}, 24 | ExtraLdFlags: "-s -w", 25 | 26 | VersionPath: "", 27 | HashPath: "", 28 | 29 | Compress: gobuild.CompressRaw, 30 | Platforms: []gobuild.Platform{{}}, 31 | } 32 | ) 33 | 34 | func main() { 35 | flag.Parse() 36 | if *flagAll { 37 | target.OutputName = fmt.Sprintf("%s-%s-%s-%s", 38 | name, 39 | gobuild.PlaceholderVersion, 40 | gobuild.PlaceholderOS, 41 | gobuild.PlaceholderArch) 42 | target.Compress = gobuild.CompressZip 43 | target.Platforms = gobuild.PlatformCommon 44 | } 45 | err := target.Build() 46 | if err != nil { 47 | panic(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/csgo-rcon/.gitignore: -------------------------------------------------------------------------------- 1 | # Output 2 | csgo-rcon 3 | 4 | # Config 5 | config.json -------------------------------------------------------------------------------- /cmd/csgo-rcon/command.example.cfg: -------------------------------------------------------------------------------- 1 | // Lines start with "//" will be treated as comment 2 | users 3 | maps * 4 | -------------------------------------------------------------------------------- /cmd/csgo-rcon/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Address": "127.0.0.1:27015", 3 | "Password": "password", 4 | "Timeout": 1 5 | } -------------------------------------------------------------------------------- /cmd/csgo-rcon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/chzyer/readline" 16 | rcon "github.com/forewing/csgo-rcon" 17 | ) 18 | 19 | // Flags of the command line 20 | type Flags struct { 21 | Address *string `json:",omitempty"` 22 | Password *string `json:",omitempty"` 23 | Timeout *float64 `json:",omitempty"` 24 | Config *string `json:",omitempty"` 25 | From *string `json:",omitempty"` 26 | Completion *string `json:",omitempty"` 27 | Interactive *bool `json:",omitempty"` 28 | } 29 | 30 | var ( 31 | flags Flags = Flags{ 32 | Address: flag.String("a", rcon.DefaultAddress, "`address` of the server RCON, in the format of HOST:PORT."), 33 | Password: flag.String("p", rcon.DefaultPassword, "`password` of the RCON."), 34 | Timeout: flag.Float64("t", rcon.DefaultTimeout.Seconds(), "`timeout` of the connection (seconds)."), 35 | Config: flag.String("c", "", "load configs from `file` instead of flags."), 36 | From: flag.String("f", "", "read commands from `file`, \"-\" for stdin. From arguments if not set."), 37 | Completion: flag.String("m", "", "read completions from `file`"), 38 | Interactive: flag.Bool("i", false, "interact with the console."), 39 | } 40 | 41 | client *rcon.Client 42 | ) 43 | 44 | func init() { 45 | flag.Parse() 46 | if len(*flags.Config) == 0 { 47 | return 48 | } 49 | data, err := ioutil.ReadFile(*flags.Config) 50 | if err != nil { 51 | fatal(err.Error()) 52 | } 53 | err = json.Unmarshal(data, &flags) 54 | if err != nil { 55 | fatal(err.Error()) 56 | } 57 | } 58 | 59 | func fatal(message string) { 60 | fmt.Fprintln(os.Stderr, message) 61 | os.Exit(1) 62 | } 63 | 64 | func main() { 65 | client = rcon.New(*flags.Address, *flags.Password, time.Duration(*flags.Timeout*float64(time.Second))) 66 | 67 | if *flags.Interactive { 68 | runInteractive() 69 | return 70 | } 71 | 72 | if len(*flags.From) > 0 { 73 | runFile(*flags.From) 74 | return 75 | } 76 | 77 | runArgs() 78 | } 79 | 80 | func runArgs() { 81 | cmd := strings.TrimSpace(strings.Join(flag.Args(), " ")) 82 | if len(cmd) == 0 { 83 | fatal("empty commands") 84 | } 85 | message, err := client.Execute(cmd) 86 | fmt.Println(strings.TrimSpace(message)) 87 | if err != nil { 88 | fatal(err.Error()) 89 | } 90 | } 91 | 92 | func runFile(filename string) { 93 | file := os.Stdin 94 | if filename != "-" { 95 | var err error 96 | file, err = os.Open(filename) 97 | if err != nil { 98 | fatal(err.Error()) 99 | } 100 | } 101 | 102 | data, err := ioutil.ReadAll(file) 103 | if err != nil { 104 | fatal(err.Error()) 105 | } 106 | 107 | message, err := client.Execute(string(data)) 108 | fmt.Println(strings.TrimSpace(message)) 109 | if err != nil { 110 | fatal(err.Error()) 111 | } 112 | } 113 | 114 | func getCommandCompletion() func(string) []string { 115 | return func(s string) []string { 116 | commands := make([]string, 0) 117 | if len(*flags.Completion) != 0 { 118 | file, err := os.Open(*flags.Completion) 119 | if err != nil { 120 | fatal(err.Error()) 121 | } 122 | defer file.Close() 123 | scanner := bufio.NewScanner(file) 124 | for scanner.Scan() { 125 | commands = append(commands, strings.Split(scanner.Text(), " ")[0]) 126 | } 127 | if err := scanner.Err(); err != nil { 128 | fatal(err.Error()) 129 | } 130 | } 131 | return commands 132 | } 133 | } 134 | 135 | var completer = readline.NewPrefixCompleter( 136 | readline.PcItem("mode", 137 | readline.PcItem("vi"), 138 | readline.PcItem("emacs"), 139 | ), 140 | readline.PcItemDynamic(getCommandCompletion()), 141 | readline.PcItem("bye"), 142 | ) 143 | 144 | func filterInput(r rune) (rune, bool) { 145 | switch r { 146 | // block CtrlZ feature 147 | case readline.CharCtrlZ: 148 | return r, false 149 | } 150 | return r, true 151 | } 152 | 153 | func runInteractive() { 154 | l, err := readline.NewEx(&readline.Config{ 155 | Prompt: "\033[34m>>> \033[0m", 156 | HistoryFile: "/tmp/csgo-rcon.tmp", 157 | AutoComplete: completer, 158 | InterruptPrompt: "^C", 159 | EOFPrompt: "exit", 160 | HistorySearchFold: true, 161 | FuncFilterInputRune: filterInput, 162 | }) 163 | if err != nil { 164 | panic(err) 165 | } 166 | defer l.Close() 167 | log.SetOutput(l.Stderr()) 168 | for { 169 | line, err := l.Readline() 170 | if err == readline.ErrInterrupt { 171 | if len(line) == 0 { 172 | break 173 | } else { 174 | continue 175 | } 176 | } else if err == io.EOF { 177 | break 178 | } 179 | line = strings.TrimSpace(line) 180 | switch { 181 | case strings.HasPrefix(line, "mode "): 182 | switch line[5:] { 183 | case "vi": 184 | l.SetVimMode(true) 185 | case "emacs": 186 | l.SetVimMode(false) 187 | default: 188 | println("invalid mode:", line[5:]) 189 | } 190 | case line == "mode": 191 | if l.IsVimMode() { 192 | println("current mode: vim") 193 | } else { 194 | println("current mode: emacs") 195 | } 196 | case line == "bye": 197 | goto exit 198 | case line == "": 199 | default: 200 | message, err := client.Execute(string(line)) 201 | fmt.Println(strings.TrimSpace(message)) 202 | if err != nil { 203 | fmt.Fprintln(os.Stderr, err) 204 | } 205 | } 206 | } 207 | exit: 208 | } 209 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/forewing/csgo-rcon 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/chzyer/logex v1.1.10 // indirect 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e 8 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23 // indirect 9 | github.com/forewing/gobuild v1.1.2 10 | golang.org/x/sys v0.0.0-20210921065528-437939a70204 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 2 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 5 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 7 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= 8 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= 11 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 12 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 13 | github.com/forewing/gobuild v1.1.2 h1:p336+WU1e0wIWttoeXWB7emKWjqD8mM7gvZ2hWy5q1c= 14 | github.com/forewing/gobuild v1.1.2/go.mod h1:f64Y49pcmjwPt4RlhwuUwjAOYo5nMkbUo/+KsaXHKT0= 15 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 16 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 17 | github.com/jackmordaunt/icns/v2 v2.1.3 h1:LBBT9k6Rvnbx+peHFNVQU9klGs0jGol/pTd0EJf0+l8= 18 | github.com/jackmordaunt/icns/v2 v2.1.3/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI= 19 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 20 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 21 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 22 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 23 | github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= 24 | github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 25 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 26 | github.com/mholt/archiver/v4 v4.0.0-alpha.5 h1:eaEYytBjRNxXhSMPJTO6G6B5NxEm1bHkVmDB1jWSVP4= 27 | github.com/mholt/archiver/v4 v4.0.0-alpha.5/go.mod h1:J7SYS/UTAtnO3I49RQEf+2FYZVwo7XBOh9Im43VrjNs= 28 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 29 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 30 | github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= 31 | github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= 32 | github.com/pierrec/lz4/v4 v4.1.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8= 33 | github.com/pierrec/lz4/v4 v4.1.12/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 34 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 38 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 41 | github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= 42 | github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= 43 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 44 | github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= 45 | github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 46 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 47 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210921065528-437939a70204 h1:JJhkWtBuTQKyz2bd5WG9H8iUsJRU3En/KRfN8B2RnDs= 52 | golang.org/x/sys v0.0.0-20210921065528-437939a70204/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 54 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 55 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 56 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 57 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | -------------------------------------------------------------------------------- /rcon.go: -------------------------------------------------------------------------------- 1 | // Package rcon provides a golang interface of Source Remote Console (RCON) client, let server operators to administer 2 | // and interact with their servers remotely in the same manner as the console provided by srcds. 3 | // Based on http://developer.valvesoftware.com/wiki/Source_RCON_Protocol 4 | package rcon 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "net" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | const ( 20 | serverdataAuth = 3 21 | serverdataAuthResponse = 2 22 | 23 | serverdataExecCommand = 2 24 | serverdataResponseValue = 0 25 | 26 | // found by trial & error 27 | maxCommandLength = 510 28 | 29 | // command (4), id (4), string1 (1), string2 (1) 30 | minMessageLength = 4 + 4 + 1 + 1 31 | // command (4), id (4), string (4096), string2 (1) 32 | maxMessageLength = 4 + 4 + 4096 + 1 33 | 34 | probablySplitIfLargerThan = maxMessageLength - 400 35 | ) 36 | 37 | const ( 38 | // DefaultAddress of the srcds RCON 39 | DefaultAddress = "127.0.0.1:27015" 40 | 41 | // DefaultPassword is empty 42 | DefaultPassword = "" 43 | 44 | // DefaultTimeout of the connection 45 | DefaultTimeout = time.Second * 1 46 | ) 47 | 48 | const ( 49 | tcpNetworkName = "tcp" 50 | authSuccess = "success" 51 | ) 52 | 53 | var ( 54 | ErrNoConnection = errors.New("no connection") 55 | ErrDialTCPFail = errors.New("dial TCP fail") 56 | ErrConnectionClosed = errors.New("connection closed") 57 | ErrBadPassword = errors.New("bad password") 58 | ErrInvalidResponse = errors.New("invalid response") 59 | ErrCrapBytes = errors.New("response contains crap bytes") 60 | ErrWaitingTimeout = errors.New("timeout while waiting for reply") 61 | ) 62 | 63 | // A Client of RCON protocol to srcds 64 | // Remember to set Timeout, it will block forever when not set 65 | type Client struct { 66 | address string 67 | password string 68 | timeout time.Duration 69 | 70 | reqID int32 71 | tcpConn *net.TCPConn 72 | 73 | lock sync.Mutex 74 | } 75 | 76 | // New return pointer to a new client, it's safe for concurrency use 77 | func New(address, password string, timeout time.Duration) *Client { 78 | c := &Client{ 79 | address: address, 80 | password: password, 81 | timeout: timeout, 82 | } 83 | if c.timeout <= 0 { 84 | c.timeout = DefaultTimeout 85 | } 86 | return c 87 | } 88 | 89 | // Execute the command. 90 | // Execute once if no "\n" provided. Return result message and nil on success, empty string and an error on failure. 91 | // If cmd includes "\n", it is treated as a script file. Splitted and trimmed into lines. Line starts with "//" will 92 | // be treated as comment and ignored. When all commands seccess, concatted messages and nil will be returned. 93 | // Once failed, concatted previous succeeded messages and an error will be returned. 94 | func (c *Client) Execute(cmd string) (string, error) { 95 | c.lock.Lock() 96 | defer c.lock.Unlock() 97 | 98 | cmds := strings.Split(cmd, "\n") 99 | if len(cmds) == 1 { 100 | return c.executeWorker(cmd) 101 | } 102 | 103 | var builder strings.Builder 104 | for i := range cmds { 105 | cmd := strings.TrimSpace(cmds[i]) 106 | if len(cmd) == 0 || strings.HasPrefix(cmd, "//") { 107 | continue 108 | } 109 | 110 | result, err := c.executeWorker(cmd) 111 | if err != nil { 112 | return builder.String(), err 113 | } 114 | 115 | builder.WriteString(result) 116 | } 117 | return builder.String(), nil 118 | } 119 | 120 | func (c *Client) executeWorker(cmd string) (string, error) { 121 | err := c.send(serverdataExecCommand, cmd) 122 | if err != nil { 123 | return c.executeRetry(cmd) 124 | } 125 | str1, err := c.receive() 126 | if err != nil { 127 | return c.executeRetry(cmd) 128 | } 129 | return str1, nil 130 | } 131 | 132 | func (c *Client) executeRetry(cmd string) (string, error) { 133 | c.disconnect() 134 | if err := c.connect(); err != nil { 135 | return "", err 136 | } 137 | c.send(serverdataAuth, c.password) 138 | 139 | auth, err := c.receive() 140 | if err != nil { 141 | return "", err 142 | } 143 | if len(auth) == 0 { 144 | auth, err := c.receive() 145 | if err != nil { 146 | return "", err 147 | } 148 | if auth != authSuccess { 149 | c.disconnect() 150 | return "", ErrBadPassword 151 | } 152 | } 153 | 154 | err = c.send(serverdataExecCommand, cmd) 155 | if err != nil { 156 | return "", err 157 | } 158 | return c.receive() 159 | } 160 | 161 | func (c *Client) disconnect() error { 162 | if c.tcpConn != nil { 163 | return c.tcpConn.Close() 164 | } 165 | return nil 166 | } 167 | 168 | func (c *Client) connect() error { 169 | conn, err := net.DialTimeout(tcpNetworkName, c.address, c.timeout) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | tcpConn, ok := conn.(*net.TCPConn) 175 | if !ok { 176 | return ErrDialTCPFail 177 | } 178 | 179 | c.tcpConn = tcpConn 180 | c.tcpConn.SetDeadline(time.Now().Add(c.timeout)) 181 | return nil 182 | } 183 | 184 | func (c *Client) send(cmd int, message string) error { 185 | if c.tcpConn == nil { 186 | return ErrNoConnection 187 | } 188 | 189 | if len(message) > maxCommandLength { 190 | return fmt.Errorf("message length exceed: %v/%v", len(message), maxCommandLength) 191 | } 192 | c.reqID++ 193 | 194 | var buffer bytes.Buffer 195 | if err := binary.Write(&buffer, binary.LittleEndian, int32(c.reqID)); err != nil { 196 | return err 197 | } 198 | if err := binary.Write(&buffer, binary.LittleEndian, int32(cmd)); err != nil { 199 | return err 200 | } 201 | buffer.WriteString(message) 202 | buffer.Write([]byte{'\x00', '\x00'}) 203 | var buffer2 bytes.Buffer 204 | if err := binary.Write(&buffer2, binary.LittleEndian, int32(buffer.Len())); err != nil { 205 | return err 206 | } 207 | if _, err := buffer.WriteTo(&buffer2); err != nil { 208 | return err 209 | } 210 | if _, err := buffer2.WriteTo(c.tcpConn); err != nil { 211 | return err 212 | } 213 | 214 | return nil 215 | } 216 | 217 | func (c *Client) receive() (string, error) { 218 | if c.tcpConn == nil { 219 | return "", ErrNoConnection 220 | } 221 | reader := bufio.NewReader(c.tcpConn) 222 | 223 | responded := false 224 | var message bytes.Buffer 225 | var message2 bytes.Buffer 226 | 227 | // response may be split into multiple packets, we don't know how many, so we loop until we decide to finish 228 | for { 229 | // read & parse packet length 230 | packetSizeBuffer := make([]byte, 4) 231 | if _, err := io.ReadFull(reader, packetSizeBuffer); err != nil { 232 | return "", ErrConnectionClosed 233 | } 234 | packetSize := int32(binary.LittleEndian.Uint32(packetSizeBuffer)) 235 | if packetSize < minMessageLength || packetSize > maxMessageLength { 236 | return "", fmt.Errorf("invalid packet size: %v", packetSize) 237 | } 238 | 239 | // read packet data 240 | packetBuffer := make([]byte, packetSize) 241 | if _, err := io.ReadFull(reader, packetBuffer); err != nil { 242 | return "", ErrConnectionClosed 243 | } 244 | 245 | // parse the packet 246 | requestID := int32(binary.LittleEndian.Uint32(packetBuffer[0:4])) 247 | if requestID == -1 { 248 | c.disconnect() 249 | return "", ErrBadPassword 250 | } 251 | if requestID != c.reqID { 252 | return "", fmt.Errorf("inconsistent requestID: %v, expected: %v", requestID, c.reqID) 253 | } 254 | 255 | responded = true 256 | response := int32(binary.LittleEndian.Uint32(packetBuffer[4:8])) 257 | if response == serverdataAuthResponse { 258 | return authSuccess, nil 259 | } 260 | if response != serverdataResponseValue { 261 | return "", ErrInvalidResponse 262 | } 263 | 264 | // split message 265 | pos1 := 8 266 | str1 := packetBuffer[pos1:packetSize] 267 | for i, b := range str1 { 268 | if b == '\x00' { 269 | pos1 += i 270 | break 271 | } 272 | } 273 | pos2 := pos1 + 1 274 | str2 := packetBuffer[pos2:packetSize] 275 | for i, b := range str2 { 276 | if b == '\x00' { 277 | pos2 += i 278 | break 279 | } 280 | } 281 | if pos2+1 != int(packetSize) { 282 | return "", ErrCrapBytes 283 | } 284 | 285 | // write messages 286 | message.Write(packetBuffer[8:pos1]) 287 | message2.Write(packetBuffer[pos1+1 : pos2]) 288 | 289 | // if no packets waiting, and last packet is small enough, stop here 290 | if _, err := reader.Peek(1); err != nil && packetSize < probablySplitIfLargerThan { 291 | break 292 | } 293 | } 294 | 295 | if !responded { 296 | return "", ErrWaitingTimeout 297 | } 298 | 299 | if message2.Len() != 0 { 300 | return "", fmt.Errorf("invalid response message: %v", message2.String()) 301 | } 302 | 303 | return message.String(), nil 304 | } 305 | --------------------------------------------------------------------------------