-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d774c2a
commit a726579
Showing
8 changed files
with
793 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.