├── .gitignore ├── LICENSE.md ├── README.md ├── factorio.go ├── packet.go ├── packet_test.go └── rcon.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Michael Pedersen 4 | Copyright (c) 2017 Greg Taylor 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | factorio-rcon 2 | ============= 3 | [![GoDoc](https://godoc.org/github.com/gtaylor/factorio-rcon?status.svg)](https://godoc.org/github.com/gtaylor/factorio-rcon) 4 | [![License](https://img.shields.io/github/license/gtaylor/factorio-rcon.svg)](https://github.com/gtaylor/factorio-rcon/blob/master/LICENSE.md) 5 | 6 | This package is a fork of [madcitygg/rcon](https://github.com/madcitygg/rcon) with a few tweaks to work with Factorio. Namely, Factorio's rejection of sending `SERVERDATA_RESPONSE_VALUE` packets to check for multi-packet responses (details [here](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses)). 7 | 8 | Usage 9 | ----- 10 | A simple example: 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | "github.com/gtaylor/factorio-rcon" 17 | ) 18 | 19 | func main() { 20 | r, err := rcon.Dial("10.10.10.10:27015") 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer r.Close() 25 | 26 | err = r.Authenticate("password") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | response, err := r.Execute("status") 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Printf("Response: %+v\n", response) 37 | } 38 | ``` 39 | 40 | License 41 | ------- 42 | 43 | Like the upstream [madcitygg/rcon](https://github.com/madcitygg/rcon), this package is licensed under the MIT License. 44 | -------------------------------------------------------------------------------- /factorio.go: -------------------------------------------------------------------------------- 1 | package rcon 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Player represents a registered player on the server. 8 | type Player struct { 9 | Name string 10 | Online bool 11 | } 12 | 13 | // CmdPlayers returns all registered players on the server. 14 | func (r *RCON) CmdPlayers() (players []Player, err error) { 15 | resp, err := r.Execute("/players") 16 | if err != nil { 17 | return 18 | } 19 | lines := strings.Split(resp.Body, "\n") 20 | for i, line := range lines { 21 | if i == 0 { 22 | // First line is header with total players listed. 23 | continue 24 | } 25 | if len(strings.TrimSpace(line)) == 0 { 26 | // Last line is just a return. Do not want. 27 | continue 28 | } 29 | 30 | var name string 31 | var online bool 32 | if strings.HasSuffix(line, "(online)") { 33 | nameStatusSplit := strings.Split(line, "(") 34 | name = strings.TrimSpace(nameStatusSplit[0]) 35 | online = true 36 | } else { 37 | name = strings.TrimSpace(line) 38 | } 39 | players = append(players, Player{Name: name, Online: online}) 40 | } 41 | return 42 | } 43 | 44 | // CmdAdmins returns all registered admin players on the server. 45 | func (r *RCON) CmdAdmins() (players []Player, err error) { 46 | resp, err := r.Execute("/admins") 47 | if err != nil { 48 | return 49 | } 50 | lines := strings.Split(resp.Body, "\n") 51 | for _, line := range lines { 52 | if len(strings.TrimSpace(line)) == 0 { 53 | // Last line is just a return. Do not want. 54 | continue 55 | } 56 | 57 | var name string 58 | var online bool 59 | if strings.HasSuffix(line, "(online)") { 60 | nameStatusSplit := strings.Split(line, "(") 61 | name = strings.TrimSpace(nameStatusSplit[0]) 62 | online = true 63 | } else { 64 | name = strings.TrimSpace(line) 65 | } 66 | players = append(players, Player{Name: name, Online: online}) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package rcon 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | ) 8 | 9 | const ( 10 | Auth int32 = 3 11 | AuthResponse int32 = 2 12 | ExecCommand int32 = 2 13 | ResponseValue int32 = 0 14 | ) 15 | 16 | const ( 17 | packetPaddingSize = 2 18 | packetHeaderFieldSize = 4 19 | packetHeaderSize = packetHeaderFieldSize * 2 20 | ) 21 | 22 | type Packet struct { 23 | Size int32 24 | ID int32 25 | Type int32 26 | Body string 27 | } 28 | 29 | func NewPacket(typ int32, body string) *Packet { 30 | var size, id int32 31 | 32 | // calculate size 33 | size = int32(len(body) + packetHeaderSize + packetPaddingSize) 34 | 35 | // assign a random request id 36 | binary.Read(rand.Reader, binary.LittleEndian, &id) 37 | 38 | // return packet 39 | return &Packet{size, id, typ, body} 40 | } 41 | 42 | func (p *Packet) Payload() (payload []byte, err error) { 43 | buffer := bytes.NewBuffer(make([]byte, 0, p.Size+packetHeaderFieldSize)) 44 | 45 | // write header fields 46 | binary.Write(buffer, binary.LittleEndian, p.Size) 47 | binary.Write(buffer, binary.LittleEndian, p.ID) 48 | binary.Write(buffer, binary.LittleEndian, p.Type) 49 | 50 | // write null-terminated string 51 | buffer.WriteString(p.Body) 52 | binary.Write(buffer, binary.LittleEndian, byte(0)) 53 | 54 | // write padding 55 | binary.Write(buffer, binary.LittleEndian, byte(0)) 56 | 57 | return buffer.Bytes(), err 58 | } 59 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package rcon 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNewPacket(t *testing.T) { 9 | p1 := NewPacket(Auth, "password") 10 | 11 | if p1.Size != 18 { 12 | t.Error("Expected packet size 18, got", p1.Size) 13 | } 14 | if p1.Type != Auth { 15 | t.Error("Expected packet type Auth(3), got", p1.Type) 16 | } 17 | if p1.Body != "password" { 18 | t.Error("Expected packet body \"password\", got", p1.Body) 19 | } 20 | 21 | p2 := NewPacket(ExecCommand, "status") 22 | if p2.Size != 16 { 23 | t.Error("Expected packet size 16, got", p2.Size) 24 | } 25 | if p2.Type != ExecCommand { 26 | t.Error("Expected packet type ExecCommand(2), got", p2.Type) 27 | } 28 | if p2.Body != "status" { 29 | t.Error("Expected packet body \"status\", got", p2.Body) 30 | } 31 | } 32 | 33 | func TestRandomID(t *testing.T) { 34 | ids := make(map[int32]bool) 35 | 36 | // generating 1000 random id's in a row is probably sufficient 37 | for i := 0; i < 1000; i++ { 38 | p := NewPacket(Auth, "pw") 39 | 40 | if ids[p.ID] { 41 | t.Error("Expected unique IDs, saw ID multiple times: ", p.ID) 42 | } 43 | ids[p.ID] = true 44 | } 45 | } 46 | 47 | func TestPayload(t *testing.T) { 48 | p := NewPacket(Auth, "password") 49 | payload, _ := p.Payload() 50 | 51 | size := payload[0:4] 52 | typ := payload[8:12] 53 | body := payload[12 : len(payload)-2] 54 | padding := payload[len(payload)-2:] 55 | 56 | if !bytes.Equal(size, []byte{18, 0, 0, 0}) { 57 | t.Error("Expected payload [0:4] to be bytes [18 0 0 0], got", payload[0:4]) 58 | } 59 | if !bytes.Equal(typ, []byte{3, 0, 0, 0}) { 60 | t.Error("Expected payload [8:12] to be bytes [3 0 0 0], got", typ) 61 | } 62 | if !bytes.Equal(body, []byte("password")) { 63 | t.Error("Expected payload body to be bytes \"password\", got", body) 64 | } 65 | if !bytes.Equal(padding, []byte("\x00\x00")) { 66 | t.Error("Expected two bytes of null padding at end of payload, got", padding) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rcon.go: -------------------------------------------------------------------------------- 1 | package rcon 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | terminationSequence = "\x00" 12 | failedAuthResponseID int32 = -1 13 | ) 14 | 15 | var ( 16 | ErrInvalidWrite = errors.New("rcon: failed to write to remote connection") 17 | ErrInvalidID = errors.New("rcon: invalid response ID from remote connection") 18 | ErrInvalidPacketOrder = errors.New("rcon: packets from server received out of order") 19 | ErrAuthFailed = errors.New("rcon: authentication failed") 20 | ) 21 | 22 | type RCON struct { 23 | // TODO: add some more useful stuff here? 24 | Address string 25 | conn net.Conn 26 | } 27 | 28 | func Dial(address string) (*RCON, error) { 29 | // dial tcp 30 | conn, err := net.Dial("tcp", address) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | // create remote console 36 | rc := &RCON{Address: address, conn: conn} 37 | return rc, nil 38 | } 39 | 40 | func (r *RCON) Close() error { 41 | return r.conn.Close() 42 | } 43 | 44 | func (r *RCON) Execute(command string) (response *Packet, err error) { 45 | // Send command to execute 46 | cmd := NewPacket(ExecCommand, command) 47 | if err = r.WritePacket(cmd); err != nil { 48 | return 49 | } 50 | 51 | response, err = r.ReadPacket() 52 | if err != nil { 53 | return 54 | } 55 | 56 | // Handle sentinel package 57 | if response.ID == cmd.ID { 58 | // append responses with same id 59 | return 60 | } else { 61 | // something has gotten out of order 62 | return nil, ErrInvalidPacketOrder 63 | } 64 | } 65 | 66 | func (r *RCON) Authenticate(password string) (err error) { 67 | // Send auth package 68 | packet := NewPacket(Auth, password) 69 | if err = r.WritePacket(packet); err != nil { 70 | return 71 | } 72 | 73 | // Get response 74 | var response *Packet 75 | response, err = r.ReadPacket() 76 | if err != nil { 77 | return 78 | } 79 | // Check that response returned correct ID 80 | if response.ID != packet.ID { 81 | return ErrInvalidID 82 | } 83 | 84 | // The server will potentially send a blank ResponseValue packet before giving 85 | // back the correct AuthResponse. This can safely be discarded, as documented here: 86 | // https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#SERVERDATA_AUTH_RESPONSE 87 | if response.Type == ResponseValue { 88 | response, err = r.ReadPacket() 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | // By now we should for sure have an AuthResponse. If we don't, there's something weird 95 | // going on server-side 96 | if response.Type != AuthResponse { 97 | panic("WTF!?") 98 | } 99 | 100 | // Check that we did not receive an ID indicating that authentication failed. 101 | if response.ID == failedAuthResponseID { 102 | return ErrAuthFailed 103 | } 104 | return 105 | } 106 | 107 | func (r *RCON) WritePacket(packet *Packet) (err error) { 108 | // generate payload 109 | var payload []byte 110 | payload, err = packet.Payload() 111 | if err != nil { 112 | return 113 | } 114 | 115 | // write payload to tcp socket 116 | var n int 117 | n, err = r.conn.Write(payload) 118 | if err != nil { 119 | return 120 | } 121 | if n != len(payload) { 122 | return ErrInvalidWrite 123 | } 124 | return 125 | } 126 | 127 | func (r *RCON) ReadPacket() (response *Packet, err error) { 128 | // Read header fields into Packet struct 129 | var packet Packet 130 | if err = binary.Read(r.conn, binary.LittleEndian, &packet.Size); err != nil { 131 | return 132 | } 133 | if err = binary.Read(r.conn, binary.LittleEndian, &packet.ID); err != nil { 134 | return 135 | } 136 | if err = binary.Read(r.conn, binary.LittleEndian, &packet.Type); err != nil { 137 | return 138 | } 139 | 140 | // Read rest of packet 141 | var n int 142 | bytesRead := 0 143 | bytesTotal := int(packet.Size - packetHeaderSize) 144 | buf := make([]byte, bytesTotal) 145 | 146 | for bytesRead < bytesTotal { 147 | n, err = r.conn.Read(buf[bytesRead:]) 148 | if err != nil { 149 | return 150 | } 151 | bytesRead += n 152 | } 153 | 154 | // Trim null bytes off body 155 | packet.Body = strings.TrimRight(string(buf), terminationSequence) 156 | 157 | // Construct final response packet 158 | return &packet, nil 159 | } 160 | --------------------------------------------------------------------------------