Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Completely rework the code #31

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
steps:

- name: Set up Go 1.21
- name: Set up Go 1.22
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Build
run: go build -v .
- name: Build icecon binary
run: go build -v ./cmd/icecon
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ![Logo](img/logo_32.png) IceCon RCON client

[![Release version](https://img.shields.io/github/release/icedream/icecon.svg?maxAge=2592000)](https://github.com/icedream/icecon/releases)
![Maintained?](https://img.shields.io/maintenance/yes/2023.svg?maxAge=2592000)
![Maintained?](https://img.shields.io/maintenance/yes/2024.svg?maxAge=2592000)

*IceCon* is a Q3-compatible RCON client. It can connect to any server that implements RCON over a Q3-compatible network protocol (UDP) and even comes with a nice, straight minimal GUI.

Expand Down
5 changes: 5 additions & 0 deletions cmd/icecon/imports_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

import (
_ "github.com/icedream/icecon/internal/ui/windows"
)
105 changes: 105 additions & 0 deletions cmd/icecon/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"fmt"
"log"
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/icedream/icecon/internal/rcon"
"github.com/icedream/icecon/internal/ui"

_ "github.com/icedream/icecon/internal/ui/console"
)

var (
flagCommand = kingpin.Flag("command",
"Run a one-off command and then exit.").
Short('c').String()
argAddress = kingpin.Arg("address",
"Server IP/hostname and port, written as \"server:port\".")
argPassword = kingpin.Arg("password", "The RCON password.")

password string
)

func usage() {
kingpin.Usage()
}

var (
hasGraphicalUI = ui.HasGraphicalUI()
flagGui *bool
)

func init() {
// only provide -gui/-g flag if there is a graphical user interface available
if hasGraphicalUI {
flagGui = kingpin.
Flag("gui", "Run as GUI (runs automatically as GUI if no arguments given, ignored if command flag used)").
Short('g').Bool()
}
}

func main() {
fmt.Println("IceCon - Icedream's RCON Client")
fmt.Println("\t\u00A9 2016-2024 Carl Kittelberger/Icedream")
fmt.Println()

argAddressTCP := argAddress.TCP()
argPasswordStr := argPassword.String()

kingpin.Parse()

// If no arguments, fall back to running the shell
wantGui := (*argAddressTCP == nil && *flagCommand == "") || *flagGui

// Command-line shell doesn't support starting up without arguments
// but graphical Windows UI does
if !(hasGraphicalUI && wantGui) {
argAddress = argAddress.Required()
argPassword = argPassword.Required()
kingpin.Parse()
}

// Initialize socket
rconClient := rcon.NewRconClient()
rconClient.InitSocket()
defer rconClient.Release()

// Set target address if given
if *argAddressTCP != nil {
rconClient.SetSocketAddr((*argAddressTCP).String())
}

// Get password
password = *argPasswordStr

// Run one-off command?
if *flagCommand != "" {
// Send
err := rconClient.Send(*flagCommand)
if err != nil {
log.Fatal(err)
return
}

// Receive
msg, err := rconClient.Receive()
if err != nil {
log.Fatal(err)
return
}
switch strings.ToLower(msg.Name) {
case "print":
fmt.Println(string(msg.Data))
}
return
}

// Which UI should be run?
if err := ui.Run(rconClient, wantGui); err != nil {
log.Fatal("User interface failed:", err)
return
}
}
4 changes: 4 additions & 0 deletions cmd/icecon/rsrc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package main

//go:generate go run -mod=mod github.com/josephspurrier/goversioninfo/cmd/goversioninfo -manifest "../../rsrc/app.manifest" -icon "../../rsrc/app.ico" -o "rsrc_windows_386.syso"
//go:generate go run -mod=mod github.com/josephspurrier/goversioninfo/cmd/goversioninfo -manifest "../../rsrc/app.manifest" -icon "../../rsrc/app.ico" -o "rsrc_windows_amd64.syso" -64
File renamed without changes.
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
module github.com/icedream/icecon

go 1.12
go 1.22

require (
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/icedream/go-q3net v0.1.0
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270
github.com/josephspurrier/goversioninfo v1.4.0
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
)

require (
github.com/akavel/rsrc v0.10.2 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
)
12 changes: 7 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
Expand All @@ -7,19 +9,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/icedream/go-q3net v0.1.0 h1:ly5QS55sXAs7HunlCPDsUmS6QLYqP6kGBdupwufaiC4=
github.com/icedream/go-q3net v0.1.0/go.mod h1:2Y0epYeaR6uWXDMvapfsUkLDqAXhI8mp/J5LxO86eUU=
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270 h1:tjLVsfoFJxX30ny02EEOjg3VXdoZA0uH8x3gw9YUM4U=
github.com/icedream/ui2walk v0.0.0-20160513005918-6f3bcb07a270/go.mod h1:6wS3BNtTpx4//e4hNWPUegvMQ9qT7iZ9RyvB8HmCtzQ=
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
Expand All @@ -29,7 +32,6 @@ golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
110 changes: 110 additions & 0 deletions internal/rcon/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package rcon

import (
"bytes"
"errors"
"fmt"
"net"
"strings"

quake "github.com/icedream/go-q3net"
)

type Client struct {
address *net.UDPAddr
addressStr string
password string

socket *net.UDPConn
socketBuffer []byte
}

func NewRconClient() *Client {
socketBuffer := make([]byte, 64*1024)
return &Client{
socketBuffer: socketBuffer,
}
}

func (c *Client) SetSocketAddr(addr string) (err error) {
newAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return
}

c.address, c.addressStr = newAddr, addr

return
}

func (c *Client) SetPassword(pw string) {
c.password = pw
}

func (c *Client) Address() *net.UDPAddr {
return c.address
}

func (c *Client) AddressString() string {
return c.addressStr
}

func (c *Client) Password() string {
return c.password
}

func (c *Client) InitSocket() (err error) {
c.socket, err = net.ListenUDP("udp", nil)
if err != nil {
return
}

return
}

func (c *Client) udpReceiveAndUnmarshal() (msg *quake.Message, err error) {
length, _, err := c.socket.ReadFromUDP(c.socketBuffer)
if err != nil {
return
}

msg, err = quake.UnmarshalMessage(c.socketBuffer[0:length])
if err != nil {
return
}
return
}

func (c *Client) Receive() (msg *quake.Message, err error) {
msg, err = c.udpReceiveAndUnmarshal()
if err != nil {
return
}
if !strings.EqualFold(msg.Name, "print") {
err = errors.New("rcon: Unexpected response from server: " + msg.Name)
}
return
}

func (c *Client) Send(input string) (err error) {
buf := new(bytes.Buffer)
msg := &quake.Message{
Header: quake.OOBHeader,
Name: "rcon",
Data: []byte(fmt.Sprintf("%s %s", c.password, input)),
}
if err = msg.Marshal(buf); err != nil {
return
}
if _, err = c.socket.WriteToUDP(buf.Bytes(), c.address); err != nil {
return
}
return
}

func (c *Client) Release() {
if c.socket != nil {
c.socket.Close()
c.socket = nil
}
}
74 changes: 74 additions & 0 deletions internal/ui/console/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ui

import (
"bufio"
"log"
"os"
"strings"

"github.com/icedream/icecon/internal/rcon"
"github.com/icedream/icecon/internal/ui"
)

func init() {
ui.RegisterUserInterface(ui.UserInterfaceProvider{
New: NewConsoleUserInterface,
})
}

type consoleUserInterface struct {
rcon *rcon.Client
bufferedStdin *bufio.Reader
}

func NewConsoleUserInterface(rconClient *rcon.Client) (ui.UserInterface, error) {
return &consoleUserInterface{
rcon: rconClient,
bufferedStdin: bufio.NewReader(os.Stdin),
}, nil
}

func (ui *consoleUserInterface) readLineFromInput() (input string, err error) {
for {
if line, hasMoreInLine, err := ui.bufferedStdin.ReadLine(); err != nil {
return input, err
} else {
input += string(line)
if !hasMoreInLine {
break
}
}
}
return
}

func (ui *consoleUserInterface) Run() error {
for {
input, err := ui.readLineFromInput()
if err != nil {
log.Fatal(err)
continue
}

// "quit" => exit shell
if strings.EqualFold(strings.TrimSpace(input), "quit") {
break
}

err = ui.rcon.Send(input)
if err != nil {
log.Println(err)
continue
}
msg, err := ui.rcon.Receive()
if err != nil {
log.Println(err)
continue
}
switch strings.ToLower(msg.Name) {
case "print":
log.Println(string(msg.Data))
}
}
return nil
}
Loading
Loading