Skip to content

Commit

Permalink
Add Minecraft input plugin (#2960)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayrdrie authored and danielnelson committed Jun 23, 2017
1 parent d774c2a commit a726579
Show file tree
Hide file tree
Showing 8 changed files with 793 additions and 0 deletions.
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

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


### 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
// 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

0 comments on commit a726579

Please sign in to comment.