Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minecraft Input Plugin using RCON #2960

Merged
merged 29 commits into from
Jun 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
48d7258
Add boilerplate for minecraft plugin
Jun 20, 2017
5cbb550
Add RCON minecraft client lib
Jun 20, 2017
9d737b7
Change rcon.go to package main
Jun 21, 2017
8fd5848
Isolate username with regex
Jun 21, 2017
a4b87da
Add username parsing test for minecraft plugin
Jun 21, 2017
96994e3
Update ParseUsername test for minecraft plugin
Jun 21, 2017
54b1778
Add method to parse scoreboard data from rcon for minecraft input plugin
Jun 22, 2017
b1c970c
Remove main.go from minecraft plugin
Jun 22, 2017
e775ba0
Change field names in RCON struct for minecraft input plugin
Jun 22, 2017
93c36f6
Add gathering of data from RCON client for minecraft input plugin
Jun 22, 2017
c3c6a8e
Fix empty username, fix uninitialized map for minecraft input plugin
Jun 22, 2017
80ed9f3
Change all instances of username to playername
Jun 22, 2017
c4fab38
Add gather test for minecraft plugin
Jun 23, 2017
db22951
Add rcon_test.go to verify that RCON server output is processed corre…
Jun 23, 2017
6a8b6b5
Remove unused variable in minecraft plugin
Jun 23, 2017
e72948e
Add README.md to minecraft plugin
Jun 23, 2017
846b487
Update minecraft input plugin README.md to conform to telegraf README…
Jun 23, 2017
6fd5718
Update README.md in minecraft input plugin to include server properti…
Jun 23, 2017
c48cf8d
Update minecraft plugin README.md to futher conform to telegraf READM…
Jun 23, 2017
f0f5f8b
Update LICENSE_OF_DEPENDENCIES.md to include license for chuckpreslar…
Jun 23, 2017
bfbb286
Update playerName tag to player in minecraft plugin
Jun 23, 2017
a0ed0cf
Update server tag format to include port in minecraft plugin
Jun 23, 2017
f850a78
Refactor regexps into package variables in minecraft plugin
Jun 23, 2017
3e4559d
Update comment spacing in minecraft plugin
Jun 23, 2017
7d33813
Add configuration defaults in minecraft package init
Jun 23, 2017
2de9da1
Add RCON network reconnect in minecraft plugin
Jun 23, 2017
66adba4
Remove extraneous comment and update formatting in minecraft plugin
Jun 23, 2017
5818f5c
Update default config file formatting in minecraft plugin
Jun 23, 2017
8ed6801
Update test data in README.md for minecraft plugin
Jun 23, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ works:
- github.com/boltdb/bolt [MIT](https://github.com/boltdb/bolt/blob/master/LICENSE)
- github.com/bsm/sarama-cluster [MIT](https://github.com/bsm/sarama-cluster/blob/master/LICENSE)
- github.com/cenkalti/backoff [MIT](https://github.com/cenkalti/backoff/blob/master/LICENSE)
- github.com/chuckpreslar/rcon [MIT](https://github.com/chuckpreslar/rcon#license)
- github.com/couchbase/go-couchbase [MIT](https://github.com/couchbase/go-couchbase/blob/master/LICENSE)
- github.com/couchbase/gomemcached [MIT](https://github.com/couchbase/gomemcached/blob/master/LICENSE)
- github.com/couchbase/goutils [MIT](https://github.com/couchbase/go-couchbase/blob/master/LICENSE)
Expand Down
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/mailchimp"
_ "github.com/influxdata/telegraf/plugins/inputs/memcached"
_ "github.com/influxdata/telegraf/plugins/inputs/mesos"
_ "github.com/influxdata/telegraf/plugins/inputs/minecraft"
_ "github.com/influxdata/telegraf/plugins/inputs/mongodb"
_ "github.com/influxdata/telegraf/plugins/inputs/mqtt_consumer"
_ "github.com/influxdata/telegraf/plugins/inputs/mysql"
Expand Down
66 changes: 66 additions & 0 deletions plugins/inputs/minecraft/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Minecraft Plugin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to stick closer to the format style in https://github.com/influxdata/telegraf/blob/master/plugins/inputs/EXAMPLE_README.md and make sure you have all the sections.


This plugin uses the RCON protocol to collect [statistics](http://minecraft.gamepedia.com/Statistics) from a [scoreboard](http://minecraft.gamepedia.com/Scoreboard) on a
Minecraft server.

To enable [RCON](http://wiki.vg/RCON) on the minecraft server, add this to your server configuration in the `server.properties` file:

```
enable-rcon=true
rcon.password=<your password>
rcon.port=<1-65535>
```

To create a new scoreboard objective called `jump` on a minecraft server tracking the `stat.jump` criteria, run this command
in the Minecraft console:

`/scoreboard objectives add jump stat.jump`

Stats are collected with the following RCON command, issued by the plugin:

`/scoreboard players list *`

### Configuration:
```
[[inputs.minecraft]]
# server address for minecraft
server = "localhost"
# port for RCON
port = "25575"
# password RCON for mincraft server
password = "replace_me"
```

### Measurements & Fields:

*This plugin uses only one measurement, titled* `minecraft`

- The field name is the scoreboard objective name.
- The field value is the count of the scoreboard objective

- `minecraft`
- `<objective_name>` (integer, count)

### Tags:

- The `minecraft` measurement:
- `server`: the Minecraft RCON server
- `player`: the Minecraft player
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure the name of this tag is correct.



### Sample Queries:

Get the number of jumps per player in the last hour:
```
SELECT SPREAD("jump") FROM "minecraft" WHERE time > now() - 1h GROUP BY "player"
```

### Example Output:

```
$ telegraf --input-filter minecraft --test
* Plugin: inputs.minecraft, Collection 1
> minecraft,player=notch,server=127.0.0.1:25575 jumps=178i 1498261397000000000
> minecraft,player=dinnerbone,server=127.0.0.1:25575 deaths=1i,jumps=1999i,cow_kills=1i 1498261397000000000
> minecraft,player=jeb,server=127.0.0.1:25575 d_pickaxe=1i,damage_dealt=80i,d_sword=2i,hunger=20i,health=20i,kills=1i,level=33i,jumps=264i,armor=15i 1498261397000000000
```
200 changes: 200 additions & 0 deletions plugins/inputs/minecraft/internal/rcon/rcon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Package rcon implements the communication protocol for communicating
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add an entry in https://github.com/influxdata/telegraf/blob/master/docs/LICENSE_OF_DEPENDENCIES.md

It may be required to add the LICENSE in this directory as well.

// with RCON servers. Tested and working with Valve game servers.
package rcon

import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net"
"strings"
)

const (
PacketPaddingSize uint8 = 2 // Size of Packet's padding.
PacketHeaderSize uint8 = 8 // Size of Packet's header.
)

const (
TerminationSequence = "\x00" // Null empty ASCII string suffix.
)

// Packet type constants.
// https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Packet_Type
const (
Exec int32 = 2
Auth int32 = 3
AuthResponse int32 = 2
ResponseValue int32 = 0
)

// Rcon package errors.
var (
ErrInvalidWrite = errors.New("Failed to write the payload corretly to remote connection.")
ErrInvalidRead = errors.New("Failed to read the response corretly from remote connection.")
ErrInvalidChallenge = errors.New("Server failed to mirror request challenge.")
ErrUnauthorizedRequest = errors.New("Client not authorized to remote server.")
ErrFailedAuthorization = errors.New("Failed to authorize to the remote server.")
)

type Client struct {
Host string // The IP address of the remote server.
Port int // The Port the remote server's listening on.
Authorized bool // Has the client been authorized by the server?
Connection net.Conn // The TCP connection to the server.
}

type Header struct {
Size int32 // The size of the payload.
Challenge int32 // The challenge ths server should mirror.
Type int32 // The type of request being sent.
}

type Packet struct {
Header Header // Packet header.
Body string // Body of packet.
}

// Compile converts a packets header and body into its approriate
// byte array payload, returning an error if the binary packages
// Write method fails to write the header bytes in their little
// endian byte order.
func (p Packet) Compile() (payload []byte, err error) {
var size int32 = p.Header.Size
var buffer bytes.Buffer
var padding [PacketPaddingSize]byte

if err = binary.Write(&buffer, binary.LittleEndian, &size); nil != err {
return
} else if err = binary.Write(&buffer, binary.LittleEndian, &p.Header.Challenge); nil != err {
return
} else if err = binary.Write(&buffer, binary.LittleEndian, &p.Header.Type); nil != err {
return
}

buffer.WriteString(p.Body)
buffer.Write(padding[:])

return buffer.Bytes(), nil
}

// NewPacket returns a pointer to a new Packet type.
func NewPacket(challenge, typ int32, body string) (packet *Packet) {
size := int32(len([]byte(body)) + int(PacketHeaderSize+PacketPaddingSize))
return &Packet{Header{size, challenge, typ}, body}
}

// Authorize calls Send with the appropriate command type and the provided
// password. The response packet is returned if authorization is successful
// or a potential error.
func (c *Client) Authorize(password string) (response *Packet, err error) {
if response, err = c.Send(Auth, password); nil == err {
if response.Header.Type == AuthResponse {
c.Authorized = true
} else {
err = ErrFailedAuthorization
response = nil
return
}
}

return
}

// Execute calls Send with the appropriate command type and the provided
// command. The response packet is returned if the command executed successfully
// or a potential error.
func (c *Client) Execute(command string) (response *Packet, err error) {
return c.Send(Exec, command)
}

// Sends accepts the commands type and its string to execute to the clients server,
// creating a packet with a random challenge id for the server to mirror,
// and compiling its payload bytes in the appropriate order. The resonse is
// decompiled from its bytes into a Packet type for return. An error is returned
// if send fails.
func (c *Client) Send(typ int32, command string) (response *Packet, err error) {
if typ != Auth && !c.Authorized {
err = ErrUnauthorizedRequest
return
}

// Create a random challenge for the server to mirror in its response.
var challenge int32
binary.Read(rand.Reader, binary.LittleEndian, &challenge)

// Create the packet from the challenge, typ and command
// and compile it to its byte payload
packet := NewPacket(challenge, typ, command)
payload, err := packet.Compile()

var n int

if nil != err {
return
} else if n, err = c.Connection.Write(payload); nil != err {
return
} else if n != len(payload) {
err = ErrInvalidWrite
return
}

var header Header

if err = binary.Read(c.Connection, binary.LittleEndian, &header.Size); nil != err {
return
} else if err = binary.Read(c.Connection, binary.LittleEndian, &header.Challenge); nil != err {
return
} else if err = binary.Read(c.Connection, binary.LittleEndian, &header.Type); nil != err {
return
}

if packet.Header.Type == Auth && header.Type == ResponseValue {
// Discard, empty SERVERDATA_RESPOSE_VALUE from authorization.
c.Connection.Read(make([]byte, header.Size-int32(PacketHeaderSize)))

// Reread the packet header.
if err = binary.Read(c.Connection, binary.LittleEndian, &header.Size); nil != err {
return
} else if err = binary.Read(c.Connection, binary.LittleEndian, &header.Challenge); nil != err {
return
} else if err = binary.Read(c.Connection, binary.LittleEndian, &header.Type); nil != err {
return
}
}

if header.Challenge != packet.Header.Challenge {
err = ErrInvalidChallenge
return
}

body := make([]byte, header.Size-int32(PacketHeaderSize))

n, err = c.Connection.Read(body)

if nil != err {
return
} else if n != len(body) {
err = ErrInvalidRead
return
}

response = new(Packet)
response.Header = header
response.Body = strings.TrimRight(string(body), TerminationSequence)

return
}

// NewClient creates a new Client type, creating the connection
// to the server specified by the host and port arguements. If
// the connection fails, an error is returned.
func NewClient(host string, port int) (client *Client, err error) {
client = new(Client)
client.Host = host
client.Port = port
client.Connection, err = net.Dial("tcp", fmt.Sprintf("%v:%v", client.Host, client.Port))
return
}
Loading