Documentation
¶
Overview ¶
Package rconserver builds Source RCON servers the way net/http builds HTTP servers: write a Handler, hand it to a Server, call ListenAndServe.
It speaks the RCON wire protocol (authentication, command framing, multi-packet chunking) so a handler only turns a command into a response. It is built on github.com/cbrgm/rcon and, like the rest of the module, depends only on the standard library.
A Server must be given a Handler and either a Password or an Authenticator; it refuses to run otherwise, to avoid an accidentally open server. A handler may be invoked concurrently for different connections, so shared state must be safe for concurrent use.
Example ¶
Serve RCON with a single password and a handler.
package main
import (
"io"
"log"
"os"
"strings"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
srv := &rconserver.Server{
Addr: ":25575",
Password: os.Getenv("RCON_PASSWORD"),
Handler: rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
switch firstWord(r.Command) {
case "list":
_, _ = io.WriteString(w, "There are 3/20 players online")
default:
_, _ = io.WriteString(w, "unknown command")
}
}),
}
log.Fatal(srv.ListenAndServe())
}
func firstWord(s string) string {
if i := strings.IndexByte(s, ' '); i >= 0 {
return s[:i]
}
return s
}
Output:
Example (RoundTrip) ¶
A complete round trip: start a server on an ephemeral port and run a command against it with a client.
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"github.com/cbrgm/rcon/rcon"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
srv := &rconserver.Server{
Password: "secret",
Handler: rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
if r.Command == "ping" {
_, _ = io.WriteString(w, "pong")
}
}),
}
go func() { _ = srv.Serve(ln) }()
defer srv.Close()
ctx := context.Background()
conn, err := rcon.Dial(ctx, ln.Addr().String(), "secret")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
out, err := conn.Execute(ctx, "ping")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
Output: pong
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrServerClosed = errors.New("rconserver: server closed")
ErrServerClosed is returned by Serve and ListenAndServe after the server is stopped by Shutdown or Close.
Functions ¶
func ListenAndServe ¶
ListenAndServe runs a server on addr with a single password and handler. It mirrors http.ListenAndServe.
Example ¶
ListenAndServe is the one-liner form.
package main
import (
"io"
"log"
"os"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
h := rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
_, _ = io.WriteString(w, "pong")
})
log.Fatal(rconserver.ListenAndServe(":25575", os.Getenv("RCON_PASSWORD"), h))
}
Output:
Types ¶
type Handler ¶
type Handler interface {
ServeRCON(w ResponseWriter, r *Request)
}
Handler responds to a single authenticated RCON command.
type HandlerFunc ¶
type HandlerFunc func(w ResponseWriter, r *Request)
HandlerFunc adapts an ordinary function to a Handler.
Example ¶
A HandlerFunc dispatches on the command and can read the client's address and the per-request context, which is canceled when the server begins shutting down.
package main
import (
"io"
"log"
"os"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
h := rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
log.Printf("command %q from %s", r.Command, r.RemoteAddr)
select {
case <-r.Context().Done():
return // server is shutting down; abandon the response
default:
}
_, _ = io.WriteString(w, "ok")
})
log.Fatal(rconserver.ListenAndServe(":25575", os.Getenv("RCON_PASSWORD"), h))
}
Output:
func (HandlerFunc) ServeRCON ¶
func (f HandlerFunc) ServeRCON(w ResponseWriter, r *Request)
ServeRCON calls f(w, r).
type Request ¶
type Request struct {
// Command is the command body sent by the client.
Command string
// RemoteAddr is the client's network address.
RemoteAddr string
// contains filtered or unexported fields
}
Request is one command from an authenticated client.
type ResponseWriter ¶
ResponseWriter accumulates a command's response. The server frames the written bytes into one or more RESPONSE_VALUE packets, splitting bodies larger than the protocol's payload cap. RCON has no headers or status, so this is just a writer.
type Server ¶
type Server struct {
// Addr is the TCP address ListenAndServe listens on.
Addr string
// Handler dispatches each authenticated command.
Handler Handler
// Password is the shared RCON password, used when Authenticator is nil.
Password string
// Authenticator validates a password. When set, it overrides Password.
Authenticator func(password string) bool
// ReadTimeout is the deadline applied before every read on a connection,
// including the initial authentication handshake, so it also bounds how
// long an unauthenticated client may hold the connection open. Zero means
// no timeout is applied anywhere on the connection — the same footgun a
// zero-value net/http.Server has, made explicit here.
ReadTimeout time.Duration
// Logger receives server events. A nil Logger discards output.
Logger *slog.Logger
// contains filtered or unexported fields
}
Server is an RCON server. A Server must have a Handler and either a Password or an Authenticator.
Example (Authenticator) ¶
An Authenticator validates the password yourself, for example to accept several passwords or look one up dynamically, instead of one shared Password.
package main
import (
"io"
"log"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
allowed := map[string]bool{
os.Getenv("ADMIN_PW"): true,
os.Getenv("DEPLOY_PW"): true,
}
delete(allowed, "") // never accept an unset password
srv := &rconserver.Server{
Addr: ":25575",
Authenticator: func(password string) bool {
return allowed[password]
},
Handler: rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
_, _ = io.WriteString(w, "ok")
}),
}
log.Fatal(srv.ListenAndServe())
}
Output:
func (*Server) Close ¶
Close stops accepting new connections and closes all active ones immediately.
func (*Server) ListenAndServe ¶
ListenAndServe listens on s.Addr and serves RCON connections.
func (*Server) Serve ¶
Serve accepts connections on ln and serves each in its own goroutine until Close or Shutdown, after which it returns ErrServerClosed.
func (*Server) Shutdown ¶
Shutdown stops accepting new connections and waits for in-flight handlers to finish, bounded by ctx, then closes all remaining connections. It returns ctx.Err() if it timed out waiting for handlers. It only waits for handlers that have begun executing by the time Shutdown checks; a command accepted moments earlier may still race past that check.
Example ¶
Shutdown stops accepting connections and drains in-flight handlers, bounded by a context. Here it is triggered by SIGINT.
package main
import (
"context"
"io"
"log"
"os"
"os/signal"
"time"
"github.com/cbrgm/rcon/rconserver"
)
func main() {
srv := &rconserver.Server{
Addr: ":25575",
Password: os.Getenv("RCON_PASSWORD"),
Handler: rconserver.HandlerFunc(func(w rconserver.ResponseWriter, r *rconserver.Request) {
_, _ = io.WriteString(w, "pong")
}),
}
go func() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
<-ctx.Done()
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutCtx)
}()
if err := srv.ListenAndServe(); err != nil && err != rconserver.ErrServerClosed {
log.Fatal(err)
}
}
Output: