rconserver

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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
}
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

View Source
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

func ListenAndServe(addr, password string, handler Handler) error

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))
}

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))
}

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.

func (*Request) Context

func (r *Request) Context() context.Context

Context returns the request's context. It is canceled when the server begins shutting down. It never returns nil; an unset context reports context.Background.

type ResponseWriter

type ResponseWriter interface {
	io.Writer
	WriteString(s string) (int, error)
}

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())
}

func (*Server) Close

func (s *Server) Close() error

Close stops accepting new connections and closes all active ones immediately.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe listens on s.Addr and serves RCON connections.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve accepts connections on ln and serves each in its own goroutine until Close or Shutdown, after which it returns ErrServerClosed.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

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)
	}
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL