Files
Gwyneth LlewelynandCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> 89531b39ee Potential fix for code scanning alert no. 11309: Clear-text logging of sensitive information
This is a quick AI-generated fix to avoid writing passwords to the logs.
There are a few packages to do just that, and I may change that in the future, but, for now, this should work fine.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-10 21:08:18 +01:00

387 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sub-project to connect to the OpenSim Remote Admin console via XML-RPC. This extra will act as a CLI application
* to send commands from the shell to the remote admin console, basically to test things; it uses the excellent idea
* from https://github.com/MarcelEdward/OpenSim-RemoteAdmin/ (a PHP tool) which reads all valid XML-RPC commands from a JSON file.
**/
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/mail"
"os"
"reflect"
"strings"
"time"
"github.com/earthboundkid/versioninfo/v2" // mostly to get the git version of this build!
// "github.com/kolo/xmlrpc"
_ "github.com/joho/godotenv/autoload" // gets value from OPENSIM_REMOTE_PASSWORD optionally from .env
"github.com/urfave/cli/v3" // main CLI engine
)
// No harm is done having just one context, which is simoly the background.
var ctx = context.Background()
func redactXMLRPCPassword(requestBody string) string {
const startTag = "<name>password</name>"
startIdx := strings.Index(requestBody, startTag)
if startIdx == -1 {
return requestBody
}
const openValueTag = "<value><string>"
const closeValueTag = "</string></value>"
valueStart := strings.Index(requestBody[startIdx:], openValueTag)
if valueStart == -1 {
return requestBody
}
valueStart += startIdx + len(openValueTag)
valueEnd := strings.Index(requestBody[valueStart:], closeValueTag)
if valueEnd == -1 {
return requestBody
}
valueEnd += valueStart
return requestBody[:valueStart] + "***REDACTED***" + requestBody[valueEnd:]
}
type XmlRpcParameter struct {
Parameter string `xmlrpc:"parameter"`
Value string `xmlrpc:"string"`
}
func main() {
var (
remoteAdminFile string = "RemoteAdmin.json"
verboseMode bool = false // just to make sure!!
opensimServerURL string
xmlrpcRawCommandsJSON map[string]any
password string
)
// Check if we have the remote password on environment.
// Note that we are using godotenv/autoload to automatically retrieve .env
// and merge with the existing environment.
password = os.Getenv("OPENSIM_REMOTE_PASSWORD")
cmd := &cli.Command{
Name: os.Args[0], // use whatever the compiled version says it uses
Authors: []any{
&mail.Address{Name: "Gwyneth Llewelyn", Address: "gwyneth.llewelyn@gwynethllewelyn.net"},
},
// HelpName: "goRemoteAdmin", // the default is fine
Usage: "Access OpenSimulator Remote Admin console via XML-RPC calls",
UsageText: `Run it from the shell with a few parameters to send commands.
You need to have a copy of a valid RemoteAdmin.json file with all the XML-RPC commands
known to OpenSimulator. You can get a copy from <https://github.com/MarcelEdward/OpenSim-RemoteAdmin/blob/master/RemoteAdmin.json>
(which served as inspiration for this command)
` + func() string {
if len(password) < 5 {
return "\nSet `OPENSIM_REMOTE_PASSWORD` to avoid passing it on the command line (inside `.env` is fine)"
}
return "\n`OPENSIM_REMOTE_PASSWORD` set to [..." + password[len(password)-4:] + "]"
}(),
EnableShellCompletion: true,
HideVersion: false,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Value: "RemoteAdmin.json",
Aliases: []string{"c"},
Usage: "Load JSON directives from `FILE`",
Destination: &remoteAdminFile,
},
&cli.StringFlag{
Name: "host",
Value: "http://127.0.0.1:9000",
Aliases: []string{"H", "url"},
Usage: "`URL` to OpenSimulator instance",
Destination: &opensimServerURL,
DefaultText: "localhost, port 9000",
},
/*
&cli.StringFlag{
Name: "user",
Value: "",
Aliases: []string{"u", "username"},
Usage: "`Username` for XML-RPC call",
Destination: &username,
DefaultText: "none, unsafe!",
},
*/
&cli.StringFlag{
Name: "password",
Value: func() string {
if len(password) < 5 {
return ""
}
return password
}(),
Aliases: []string{"p"},
Usage: "Access `password` or 'secret' for XML-RPC call",
Destination: &password,
DefaultText: func() string {
if len(password) < 5 {
return "none, unsafe — set `OPENSIM_REMOTE_PASSWORD` instead"
}
return "set from OPENSIM_REMOTE_PASSWORD=[..." + password[len(password)-4:] + "]"
}(),
Action: func(ctx context.Context, c *cli.Command, value string) error {
if len(password) < 5 {
return fmt.Errorf("empty or too small password (length %d)", len(password))
}
return nil
},
},
&cli.BoolFlag{
Name: "verbose",
Value: false,
Aliases: []string{"w", "v", "debug"},
Usage: "Verbose/debug mode (shows lots of info)",
Destination: &verboseMode,
DefaultText: "false",
},
&cli.BoolFlag{
Name: "version",
Aliases: []string{"V"},
Usage: "Print the version",
Action: func(ctx context.Context, c *cli.Command, value bool) error {
fmt.Printf("VERSION:\n%s\n", versioninfo.Short())
return nil
},
},
},
CommandNotFound: func(ctx context.Context, cmd *cli.Command, command string) {
fmt.Fprintf(cmd.Writer, "The command %q doesn't seem to be valid for OpenSimulator Remote Admin.\n", command)
},
Action: func(ctx context.Context, cmd *cli.Command) error {
if len(os.Args) < 2 {
return fmt.Errorf("empty command line; try --help to get a list of available commands")
}
var oneRawCommand, oneRawProperty, oneRawSubProperty map[string]any
var ok bool
if verboseMode && xmlrpcRawCommandsJSON != nil {
//fmt.Printf("%#v", xmlrpcRawCommandsJSON)
fmt.Println("--- Command list dump below ---")
for rawCommand, rawCommandData := range xmlrpcRawCommandsJSON {
fmt.Println("\n\n====================\nCommand", rawCommand, ":\n====================")
oneRawCommand, ok = rawCommandData.(map[string]any)
if ok {
for property, rawData := range oneRawCommand {
fmt.Println("Property:", property, "\nData:")
// see if data has more slices/maps inside
if reflect.TypeOf(rawData).Kind() != reflect.String {
oneRawProperty, ok = rawData.(map[string]any)
if ok {
for subProperty, rawSubPropertyData := range oneRawProperty {
fmt.Println("\t", subProperty, ">")
// see if this subproperty has more slices/maps inside... (we go another level deeper!)
if reflect.TypeOf(rawSubPropertyData).Kind() != reflect.String {
oneRawSubProperty, ok = rawSubPropertyData.(map[string]any)
if ok {
for subSubProperty, rawSubSubPropertyData := range oneRawSubProperty {
fmt.Println("\t\t", subSubProperty, ":>", rawSubSubPropertyData)
}
}
} else {
fmt.Println("\t\t", rawSubPropertyData)
}
}
}
} else { // rawData is just a string
fmt.Println("\t", rawData)
}
fmt.Println("")
}
} else {
fmt.Println("(couldn't extract properties)")
}
}
}
return nil
},
}
/*
if xmlrpcRawCommandsJSON == nil || cmd == nil {
fmt.Println("Please place the JSON Remote Admin file on the path\n(you can get it from here: https://github.com/MarcelEdward/OpenSim-RemoteAdmin/blob/master/RemoteAdmin.json)\nand try again")
os.Exit(2) // ENOENT
}
*/
cmd.Copyright = "Licensed as CC-BY " + formatAsYear(time.Now()) + " by " + cmd.Authors[0].(*mail.Address).Name + ". Few rights reserved."
// add xmlrpc commands on demand!!
if remoteAdminFile == "" {
if verboseMode {
fmt.Println("Empty JSON config file name, file not loaded, and no XML-RPC commands will be available")
}
} else {
// Open our jsonFile
// if we os.Open returns an error then handle it
if jsonFile, err := os.Open(remoteAdminFile); err == nil {
if verboseMode {
fmt.Println("Successfully Opened", remoteAdminFile)
}
// defer the closing of our jsonFile so that we can parse it later on
defer jsonFile.Close()
if byteValue, err := io.ReadAll(jsonFile); err == nil {
json.Unmarshal([]byte(byteValue), &xmlrpcRawCommandsJSON)
} else {
if verboseMode {
fmt.Println("Could not load", remoteAdminFile)
}
}
} else {
if verboseMode {
fmt.Println("Could not open file", remoteAdminFile)
}
}
// now try to add the commands to the App
if xmlrpcRawCommandsJSON == nil {
fmt.Println("No JSON config file specified or file invalid; limited functionality applies")
} else {
// First attempt: just add commands, if they don't work, tough!
// var oneRawCommand, oneRawProperty, oneRawSubProperty map[string]interface{}
var usage string
if verboseMode {
fmt.Println("--- Loading commands ---")
}
for rawCommand, rawCommandData := range xmlrpcRawCommandsJSON {
// there should be one
oneRawCommand, ok := rawCommandData.(map[string]any)
if !ok {
if verboseMode {
fmt.Print("x")
}
break // try next command...
}
if rawUsage, ok := oneRawCommand["description"]; ok {
usage = fmt.Sprintf("%v", rawUsage)
} else {
usage = "(no description)"
}
var newCommand cli.Command
newCommand = cli.Command{
Name: rawCommand,
Usage: usage,
Action: func(ctx context.Context, cmd *cli.Command) error {
fmt.Println("Sending", cmd.Name, "with", cmd.Args(), "to", opensimServerURL)
if !cmd.Args().Present() {
return fmt.Errorf("No arguments found for command %q - aborting!\n", rawCommand)
}
// Build the XML manually, since it's too hard to do it using a library...
var xmlrpcRequest bytes.Buffer
xmlrpcRequest.WriteString(`<?xml version="1.0"?>
<methodCall>
<methodName>` + cmd.Name + `</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>password</name>
<value><string>` + password + `</string></value>
</member>`)
for i := 0; i < cmd.NArg()-1; i += 2 {
xmlrpcRequest.WriteString("\n\t\t\t\t\t\t<member>\n\t\t\t\t\t\t\t<name>" + cmd.Args().Get(i) + "</name>\n")
xmlrpcRequest.WriteString("\t\t\t\t\t\t\t<value>" + cmd.Args().Get(i+1) + "</value>\n\t\t\t\t\t\t</member>\n")
}
xmlrpcRequest.WriteString(` </struct>
</value>
</param>
</params>
</methodCall>`)
// return fmt.Errorf("Could not initialise buffer for request; error was: %q\n", err.Error())
// should do some sanitation here
if verboseMode {
fmt.Printf("Request: %v\n", redactXMLRPCPassword(xmlrpcRequest.String()))
}
var client http.Client
if req, err := http.NewRequest("POST", opensimServerURL, &xmlrpcRequest); err != nil {
return fmt.Errorf("Could not initialise request with error: %q\n", err.Error())
} else {
req.Header.Add("Content-type", "text/xml")
req.Header.Add("Connection", "close")
req.ContentLength = int64(xmlrpcRequest.Len())
resp, err := client.Do(req)
if err == nil {
fmt.Printf("Response: %#v\n", resp)
// decode resp to get valid XML, etc.se
// close response to do TCP keepalive
if errClose := resp.Body.Close(); errClose != nil {
return fmt.Errorf("XML-RPC: could not close response body, error was: %q", errClose)
}
}
return err
}
/*
// It's hard to extract the parameters to do what I want! This is using "github.com/kolo/xmlrpc"
// loop through parameter/value pairs
var xmlrpcRequest []XmlRpcParameter
// add password first
xmlrpcRequest = append(xmlrpcRequest, XmlRpcParameter{
Parameter: "password",
Value: password,
})
for i := 2; i < c.NArg() - 1; i+=2 {
xmlrpcRequest = append(xmlrpcRequest, XmlRpcParameter{
Parameter: c.Args().Get(i),
Value: c.Args().Get(i+1),
})
}
fmt.Printf("Request: %v\n", xmlrpcRequest)
if client, err := xmlrpc.NewClient(opensimServerURL, nil); err != nil {
return fmt.Errorf("Could not create a new client to %q - aborting!\n", opensimServerURL)
} else {
var response xmlrpc.Response
err := client.Call(c.Command.Name, &xmlrpcRequest, &response)
fmt.Printf("Response: %v\n", response)
return err
}
return nil
*/
},
}
cmd.Commands = append(cmd.Commands, &newCommand)
if verboseMode {
fmt.Print(".")
}
}
if verboseMode {
fmt.Println("\nAll found commands loaded.")
}
}
}
if err := cmd.Run(ctx, os.Args); err != nil {
fmt.Println(err)
os.Exit(22) // EINVAL
}
}
// formatAsYear extracts the year drom a string and returns it as a valid string.
func formatAsYear(t time.Time) string {
year, _, _ := t.Date()
return fmt.Sprintf("%d", year)
}