Skip to content

Commit

Permalink
Add Custom RCON Broadcast Source Code (#9)
Browse files Browse the repository at this point in the history
# Add Custom RCON Broadcast Source Code

## Summary

This pull request adds the custom RCON broadcast source code to the
project, following the feature introduced in PR #5. The changes
encompass the inclusion of essential code, dependencies, and updates to
the Dockerfile to facilitate the build and integration of the custom
RCON broadcast binary.

## Motivation and Context

The addition of the custom RCON broadcast feature, as outlined in PR #5,
is needed for the capability to broadcast messages with spaces (not
supported natively as spaces break the message).

After a lot of tries and using either underscores `_` or dashes `-` to
concatenate the messages, I thought that it just isn't readable or
pleasant at all.

I did some research with 'invisible' spaces (other characters that would
replace the spaces but would be invisible) and ended up encountering
this information about [ASCII Code for
NBSP](https://www.ascii-code.com/character/nbsp) and a [proof of
concept](https://github.com/Darkhand81/Palworld_broadcast_encoding_bug)
by @Darkhand81 via [this Reddit
post](https://www.reddit.com/r/Palworld/comments/1aplmvw/ive_figured_out_the_palworld_broadcast_command/)
so I decided to code it a robust solution in Go so it could be compiled
and used inside the container like @gorcon's rcon-cli.

## Description

The primary changes include:

- Introduction of the custom RCON broadcast source code to the project.
- Inclusion of necessary dependencies.
- Updates to the Dockerfile to ensure the build process incorporates the
custom RCON broadcast binary.

## Testing Instructions

To validate these changes:

1. Build the project and ensure the custom RCON broadcast binary is
successfully compiled.
2. Run the server with the new image.
3. Verify that the RCON broadcast command now supports spaces.

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have updated the documentation (if necessary)
- [x] My changes do not introduce any breaking changes or bugs
  • Loading branch information
thejcpalma authored Mar 4, 2024
2 parents e69b282 + fcd0f66 commit de777d1
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 4 deletions.
13 changes: 9 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Description: Dockerfile for Palworld Dedicated Server

# Build the rcon binary
# Build the rcon binaries (GORCON and custom rcon broadcast built by @thejcpalma)
FROM golang:1.22.0-bookworm as rcon-build

WORKDIR /build
Expand All @@ -19,6 +19,12 @@ RUN curl -fsSLO "$GORCON_RCONCLI_URL" \
&& rm -Rf "$GORCON_RCONCLI_DIR" \
&& go build -v ./cmd/gorcon

WORKDIR /build/custom_rcon_broadcast/

# Build the custom rcon broadcast binary
COPY /src/custom_rcon_broadcast/ .
RUN go build -v -o /build/rcon_broadcast main.go

# Build the supercronic binary
FROM debian:bookworm-slim as supercronic-build

Expand Down Expand Up @@ -61,17 +67,16 @@ RUN apt-get update \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Copy the rcon and supercronic binaries
# Copy the rcon, custom rcon broadcast (to fix spaces in the message) and supercronic binaries
COPY --from=rcon-build --chmod=755 /build/gorcon /usr/local/bin/rcon
COPY --from=rcon-build --chmod=755 /build/rcon_broadcast /usr/local/bin/rcon_broadcast
COPY --from=supercronic-build --chmod=755 /usr/local/bin/supercronic /usr/local/bin/supercronic

ENV APP_ID=2394010
ENV SERVER_DIR=/home/steam/server

COPY --chown=steam:steam --chmod=755 scripts/ ${SERVER_DIR}/scripts
COPY --chown=steam:steam --chmod=755 entrypoint.sh ${SERVER_DIR}/
# Copy custom rcon broadcast to fix spaces in the message
COPY --chmod=755 bin/rcon_broadcast /usr/local/bin/rcon_broadcast

RUN ln -s ${SERVER_DIR}/scripts/backup_manager.sh /usr/local/bin/backup_manager \
&& ln -s ${SERVER_DIR}/scripts/rcon/rconcli.sh /usr/local/bin/rconcli \
Expand Down
Binary file removed bin/rcon_broadcast
Binary file not shown.
5 changes: 5 additions & 0 deletions src/custom_rcon_broadcast/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module custom_rcon_broadcast

go 1.22

require gopkg.in/yaml.v2 v2.4.0
4 changes: 4 additions & 0 deletions src/custom_rcon_broadcast/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
175 changes: 175 additions & 0 deletions src/custom_rcon_broadcast/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
Author: João Palma
Project Name: thejcpalma/palworld-dedicated-server-docker
GitHub: https://github.com/thejcpalma/palworld-dedicated-server-docker
DockerHub: https://hub.docker.com/r/thejcpalma/palworld-dedicated-server
*/

package main

import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"os"
"log"
"net"
"strconv"
"strings"
"gopkg.in/yaml.v2"
)

// ServerConfig is a structure that holds the configuration details for the RCON server.
// It is used to unmarshal the YAML configuration file provided by the user.
// The struct has one field, Default, which is an anonymous struct.
//
// The Default struct has two fields:
// Address: This is a string that holds the IP address and port of the RCON server.
// It is expected to be in the format "ip:port".
// Password: This is a string that holds the password for the RCON server.
//
// The yaml tags are used to map the struct fields to the corresponding keys in the YAML file.
type ServerConfig struct {
Default struct {
Address string `yaml:"address"` // The IP address and port of the RCON server
Password string `yaml:"password"` // The password for the RCON server
} `yaml:"default"` // The default configuration for the RCON server
}


// sendRconCommand sends a command to a RCON server and returns the server's response.
// It establishes a TCP connection to the server, authenticates using the provided password,
// sends the command, and reads the server's response.
// sendRconCommand sends an RCON command to a server and returns the server's response.
// It establishes a TCP connection to the RCON server using the provided IP address and port.
// The authentication packet is created with the provided password and sent to the server.
// If the authentication is successful, a command packet is created with the provided command and sent to the server.
// The server's response to the command packet is read and returned as a string.
// If any error occurs during the process, an error is returned.
func sendRconCommand(ip string, port int, password string, command string) (string, error) {
// Establish a TCP connection to the RCON server.
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
if err != nil {
return "", err
}
defer conn.Close()

// Create an authentication packet.
// The packet consists of a little-endian int32 length field, a little-endian int32 request ID field,
// a little-endian int32 type field, the password as a null-terminated string, and two null bytes.
authPacket := new(bytes.Buffer)
binary.Write(authPacket, binary.LittleEndian, int32(10+len(password))) // Length
binary.Write(authPacket, binary.LittleEndian, int32(1)) // Request ID
binary.Write(authPacket, binary.LittleEndian, int32(3)) // Type
authPacket.WriteString(password) // Password
authPacket.Write([]byte{0, 0}) // Null bytes

// Send the authentication packet to the server.
_, err = conn.Write(authPacket.Bytes())
if err != nil {
return "", err
}

// Read the server's response to the authentication packet.
authResponse := make([]byte, 4096)
_, err = conn.Read(authResponse)
if err != nil {
return "", err
}

// Check the server's response to the authentication packet.
// If the response ID is -1, the authentication failed.
responseID := int32(binary.LittleEndian.Uint32(authResponse[4:8]))
if responseID == -1 {
return "", fmt.Errorf("authentication failed")
}

// Create a command packet.
// The packet consists of a little-endian int32 length field, a little-endian int32 request ID field,
// a little-endian int32 type field, the command as a null-terminated string, and two null bytes.
commandBytes := append([]byte(command), 0, 0)
commandPacket := new(bytes.Buffer)
binary.Write(commandPacket, binary.LittleEndian, int32(10+len(commandBytes)-2)) // Length
binary.Write(commandPacket, binary.LittleEndian, int32(2)) // Request ID
binary.Write(commandPacket, binary.LittleEndian, int32(2)) // Type
commandPacket.Write(commandBytes) // Command

// Send the command packet to the server.
_, err = conn.Write(commandPacket.Bytes())
if err != nil {
return "", err
}

// Read the server's response to the command packet.
responsePacket := make([]byte, 4096)
_, err = conn.Read(responsePacket)
if err != nil {
return "", err
}

// Extract the body of the server's response from the response packet.
responseBody := string(responsePacket[12 : len(responsePacket)-2])

return responseBody, nil
}


// main is the entry point of the program.
// It parses command-line flags, reads a configuration file, sends the 'broadcast' command to the Palworld RCON server,
// and prints the server's response.
func main() {
// Parse command-line flags
configPath := flag.String("c", "", "Path to the YAML configuration file")
flag.Parse()

// Check if a configuration file was provided
if *configPath == "" {
log.Fatal("Please provide a configuration file with the -c flag.")
}

// Read the configuration file
configData, err := os.ReadFile(*configPath)
if err != nil {
log.Fatal(err)
}

// Parse the configuration file
var config ServerConfig
err = yaml.Unmarshal(configData, &config)
if err != nil {
log.Fatal(err)
}

// Split the address into IP and port
splitAddress := strings.Split(config.Default.Address, ":")
ip := splitAddress[0]
port, err := strconv.Atoi(splitAddress[1])
if err != nil {
log.Fatal(err)
}

// Check if a message was provided
if len(flag.Args()) < 1 {
log.Fatal("Please provide a message as a command-line argument.")
}

// Get the message from the command-line arguments
message := flag.Args()[0]
modded_message := strings.ReplaceAll(message, " ", "\xa0") // Replace spaces with non-breaking spaces
command := "broadcast " + modded_message // Construct the command

// Send the command to the RCON server and print the server's response.
result, err := sendRconCommand(ip, port, config.Default.Password, command)
if err != nil {
log.Fatal(err)
}

// Check if the server's response indicates that the message was broadcasted
if strings.HasPrefix(result, "Broadcasted:") {
// Prints the message that was broadcasted
fmt.Println("Broadcasted: " + message)
} else {
fmt.Println("Error on broadcast!")
}
}

0 comments on commit de777d1

Please sign in to comment.