-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
Changes from all commits
48d7258
5cbb550
9d737b7
8fd5848
a4b87da
96994e3
54b1778
b1c970c
e775ba0
93c36f6
c3c6a8e
80ed9f3
c4fab38
db22951
6a8b6b5
e72948e
846b487
6fd5718
c48cf8d
f0f5f8b
bfbb286
a0ed0cf
f850a78
3e4559d
7d33813
2de9da1
66adba4
5818f5c
8ed6801
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
// Package rcon implements the communication protocol for communicating | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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.