Skip to content

Commit

Permalink
support a default rcon server
Browse files Browse the repository at this point in the history
  • Loading branch information
holysoles committed Feb 8, 2024
1 parent 06b203b commit 0269d50
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 52 deletions.
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ COPY *.go ./
# run the build
RUN CGO_ENABLED=0 GOOS=linux go build -o /ginrcon



# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...
Expand All @@ -23,5 +21,11 @@ WORKDIR /

COPY --from=build-stage /ginrcon /ginrcon

ENV PORT=8080 \
TRUSTED_PROXIES= \
RCON_SERVER= \
RCON_PORT= \
RCON_ADMIN_PASSWORD=

EXPOSE 8080
ENTRYPOINT ["/ginrcon"]
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
# GinRCON
A lightweight, simple Go webserver build on Gin to provide a REST API frontend to sending RCON commands to a game server.

## Calling the API

## API Call Examples
### Send command to default RCON server
Assuming you have a default RCON server specified (see environment variables)
```bash
curl --location 'http://localhost:8080/command' \
--header 'Content-Type: application/json' \
--data '{
"command": "Save"
}'
```
### POST command to specific RCON server
```bash
curl --location 'http://localhost:8080/command' \
--header 'Content-Type: application/json' \
--data '{
"server": "gameserver:25575",
"password": "1234",
"command": "Save"
}'
```
### Health check the status of the webserver
```bash
curl --location 'http://localhost:8080/status'
```
## Docker
### Image
A docker image that runs this application is available, `ghcr.io/holysoles/ginrcon`.
A docker image that runs this application is available under the repo packages, or at `ghcr.io/holysoles/ginrcon`.
### Environment Variables
- `PORT`: Optional, override the port of the webserver within the container
- `PORT`: Optional, override the port (default 8080) of the webserver within the container
- `TRUSTED_PROXIES`: Optional, set specified trusted proxy addresses
- `RCON_SERVER`: Optional, configure a default RCON server's hostname
- `RCON_PORT`: Optional, configure a default RCON server's port
- `RCON_ADMIN_PASSWORD`: Optional, configure the password to a default RCON server
### Compose
An example compose file can be found in the repo [here](https://github.com/holysoles/ginrcon/blob/master/compose.yaml)
### Building
Expand Down
5 changes: 4 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ services:
- "8085:8080/tcp"
environment:
PORT: 8080
TRUSTED_PROXIES: "192.168.1.2"
TRUSTED_PROXIES: "192.168.1.2"
RCON_SERVER: gameserver
RCON_PORT: 25575
RCON_ADMIN_PASSWORD: flyhigh
119 changes: 74 additions & 45 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,63 @@
package main

import (
"errors"
"fmt"
"net"
"net/http"
"os"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/gorcon/rcon"
)

type rconInfo struct {
Server string `form:"server" json:"server" xml:"server" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
Command string `form:"command" json:"command" xml:"command" binding:"required"`
var (
gConn *rcon.Conn

ErrNoDefaultConnection = errors.New("no connection details were specified and no valid default connection exists")
ErrInvalidConnectionDetails = errors.New("invalid connection details to the rcon server were provided")
ErrInvalidResponseFromRcon = errors.New("an invalid response was received from the rcon server")
)

type openConnInfo struct {
Server string `form:"server" json:"server" xml:"server"`
Password string `form:"password" json:"password" xml:"password"`
}
type rconReply struct {
Message string `json:"message"`
type commandReq struct {
openConnInfo
Command string `form:"command" json:"command" xml:"command" binding:"required"`
}
type errorResponse struct {
Error string
type commandRes struct {
Message string `json:"message"`
}

func main() {
initDefaultRcon()
initWeb()
}

func initDefaultRcon() {
var err error
rconHostPort := net.JoinHostPort(os.Getenv("RCON_SERVER"), os.Getenv("RCON_PORT"))
rconAdminPass := os.Getenv("RCON_ADMIN_PASSWORD")
info := openConnInfo{Server: rconHostPort, Password: rconAdminPass}

//check env vars to construct
gConn, err = openRcon(info)
//log error as a warning, if we have bad default info just throw it away
if err == ErrInvalidConnectionDetails {
fmt.Println("default RCON server connection details were not provided or invalid")
return
} else if err != nil {
fmt.Println(err)
return
} else {
fmt.Println("successfully opened default RCON connection to", rconHostPort)
}
}

func initWeb() {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r.ForwardedByClientIP = true
Expand All @@ -34,36 +67,45 @@ func main() {
r.GET("/status", healthCheck)
r.POST("/command", processCommand)

r.Run(":" + os.Getenv("PORT"))
bindPort, customBind := os.LookupEnv("PORT")
if customBind {
r.Run(":" + bindPort)
} else {
r.Run()
}
}

func processCommand(c *gin.Context) {
var info rconInfo
var info commandReq
err := c.Bind(&info)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}

err = info.validateConnectionInfo()
if err != nil {
c.JSON(http.StatusBadGateway, errorResponse{"Failed to test TCP connection to provided server"})
return
}

conn, err := rcon.Dial(info.Server, info.Password)
if err != nil {
switch err {
case rcon.ErrAuthNotRCON:
case rcon.ErrInvalidAuthResponse:
c.AbortWithStatus(http.StatusInternalServerError)
case rcon.ErrAuthFailed:
fallthrough
default:
c.AbortWithStatus(http.StatusUnauthorized)
// if they passed connection info, we should try to create a new connection
var conn *rcon.Conn
if info.openConnInfo.Server != "" {
fmt.Println("using provided connection info for incoming request")
conn, err = openRcon(info.openConnInfo)
if err != nil {
switch err {
case ErrInvalidResponseFromRcon:
c.AbortWithError(http.StatusInternalServerError, err)
case ErrInvalidConnectionDetails:
c.AbortWithError(http.StatusUnauthorized, err)
default: //assume we screwed up
c.AbortWithError(http.StatusInternalServerError, err)
}
return
}
defer conn.Close()
} else if gConn != nil {
fmt.Println("using default server connection for incoming request")
conn = gConn
} else {
// no valid default connection and no credentials provided
c.AbortWithError(http.StatusBadGateway, ErrNoDefaultConnection)
}

msg, err := conn.Execute(info.Command)
if err != nil {
switch err {
Expand All @@ -74,26 +116,13 @@ func processCommand(c *gin.Context) {
default:
c.AbortWithStatus(http.StatusInternalServerError)
}
return
}
res := rconReply{msg}
res := commandRes{msg}

c.JSON(http.StatusOK, res)
}

func (i *rconInfo) validateConnectionInfo() error {
validate := validator.New(validator.WithRequiredStructEnabled())
err := validate.Var(i.Server, "required,hostname_port")
if err != nil {
return err
}
dialer := &net.Dialer{Timeout: time.Second * 1}
_, err = dialer.Dial("tcp", i.Server)
if err != nil {
return err
}
return nil
}

func healthCheck(c *gin.Context) {
c.Status(http.StatusOK)
}
47 changes: 47 additions & 0 deletions rcon_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

import (
"net"
"time"

"github.com/go-playground/validator/v10"
"github.com/gorcon/rcon"
)

func openRcon(i openConnInfo) (*rcon.Conn, error) {
// basic validation so we can return more verbose error messages
err := i.validateConnectionInfo()
if err != nil {
return nil, ErrInvalidConnectionDetails
}

//actually open
conn, err := rcon.Dial(i.Server, i.Password)
if err != nil {
switch err {
case rcon.ErrAuthNotRCON:
fallthrough
case rcon.ErrInvalidAuthResponse:
return nil, ErrInvalidResponseFromRcon
case rcon.ErrAuthFailed:
fallthrough
default:
return nil, ErrInvalidConnectionDetails
}
}
return conn, nil
}

func (i *openConnInfo) validateConnectionInfo() error {
validate := validator.New(validator.WithRequiredStructEnabled())
err := validate.Var(i.Server, "required,hostname_port")
if err != nil {
return err
}
dialer := &net.Dialer{Timeout: time.Second * 1}
_, err = dialer.Dial("tcp", i.Server)
if err != nil {
return err
}
return nil
}

0 comments on commit 0269d50

Please sign in to comment.