wow: init

This is a shitty MMORPG server. Private. Do not touch.

Change-Id: Iddfce069f5895632d305a73fcaa2d963e25dc600
diff --git a/personal/q3k/wow/panel/soap.go b/personal/q3k/wow/panel/soap.go
new file mode 100644
index 0000000..1019861
--- /dev/null
+++ b/personal/q3k/wow/panel/soap.go
@@ -0,0 +1,215 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/xml"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/golang/glog"
+)
+
+type Envelope struct {
+	XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
+	Body    *Body
+}
+
+type Body struct {
+	XMLName  xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
+	Request  *Request
+	Response *Response
+	Fault    *Fault
+}
+
+type Request struct {
+	XMLName xml.Name `xml:"urn:AC executeCommand"`
+	Command string   `xml:"command"`
+}
+type Response struct {
+	XMLName xml.Name `xml:"urn:AC executeCommandResponse"`
+	Result  string   `xml:"result"`
+}
+
+type Fault struct {
+	XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"`
+	Code    string   `xml:"faultcode"`
+	String  string   `xml:"faultstring"`
+}
+
+type commandRes struct {
+	result string
+	fault  string
+}
+
+var (
+	reAccount  = regexp.MustCompile(`^[a-z0-9\-_\.]{3,16}$`)
+	rePassword = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]{3,16}$`)
+)
+
+func createAccount(ctx context.Context, name, password string) error {
+	if !reAccount.MatchString(name) {
+		return fmt.Errorf("invalid account name")
+	}
+	if !rePassword.MatchString(password) {
+		return fmt.Errorf("invalid password name")
+	}
+	res, err := runCommand(ctx, fmt.Sprintf("account create %s %s", name, password))
+	if err != nil {
+		glog.Errorf("Account create: %v", err)
+		return fmt.Errorf("server unavailable")
+	}
+	if res.result == fmt.Sprintf("Account created: %s", name) {
+		glog.Infof("Created account %q", name)
+		return nil
+	}
+	glog.Errorf("Account create fault: %q/%q", res.fault, res.result)
+	return fmt.Errorf("server error")
+}
+
+func ensureAccount(ctx context.Context, name, password string) error {
+	if !reAccount.MatchString(name) {
+		return fmt.Errorf("invalid account name")
+	}
+	if !rePassword.MatchString(password) {
+		return fmt.Errorf("invalid password name")
+	}
+	res, err := runCommand(ctx, fmt.Sprintf("account create %s %s", name, password))
+	if err != nil {
+		glog.Errorf("Account create: %v", err)
+		return fmt.Errorf("server unavailable")
+	}
+	if res.result == fmt.Sprintf("Account created: %s", name) {
+		glog.Infof("Created account %q", name)
+		return nil
+	}
+	if res.fault != "Account with this name already exist!" {
+		glog.Errorf("Account create fault: %q/%q", res.fault, res.result)
+		return fmt.Errorf("server error")
+	}
+
+	res, err = runCommand(ctx, fmt.Sprintf("account set password %s %s %s", name, password, password))
+	if res.result == "The password was changed" {
+		glog.Infof("Updated password for account %q", name)
+		return nil
+	}
+	glog.Infof("password update fault: %q/%q", res.fault, res.result)
+	return fmt.Errorf("server error")
+}
+
+func runCommand(ctx context.Context, cmd string) (*commandRes, error) {
+	data, err := xml.Marshal(&Envelope{
+		Body: &Body{
+			Request: &Request{
+				Command: cmd,
+			},
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("marshal: %w", err)
+	}
+	buf := bytes.NewBuffer(data)
+	req, err := http.NewRequestWithContext(ctx, "POST", flagSOAPAddress, buf)
+	if err != nil {
+		return nil, fmt.Errorf("NewRequest(POST, %q): %w", flagSOAPAddress, err)
+	}
+
+	req.SetBasicAuth(flagSOAPUsername, flagSOAPPassword)
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("req.Do: %w", err)
+	}
+	defer resp.Body.Close()
+
+	respBytes, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("ReadAll response: %w", err)
+	}
+
+	respEnvelope := Envelope{}
+	err = xml.Unmarshal(respBytes, &respEnvelope)
+	if err != nil {
+		return nil, fmt.Errorf("unmarshal: %w", err)
+	}
+
+	if respEnvelope.Body == nil {
+		return nil, fmt.Errorf("no body returned")
+	}
+
+	if respEnvelope.Body.Fault != nil {
+		fault := respEnvelope.Body.Fault
+		if fault.Code == "SOAP-ENV:Client" {
+			return &commandRes{
+				fault: strings.TrimSpace(fault.String),
+			}, nil
+		}
+		return nil, fmt.Errorf("SOAP error %q: %v", fault.Code, fault.String)
+	}
+
+	result := ""
+	if respEnvelope.Body.Response != nil {
+		result = respEnvelope.Body.Response.Result
+	}
+
+	return &commandRes{
+		result: strings.TrimSpace(result),
+	}, nil
+}
+
+type playerinfo struct {
+	Account   string
+	Character string
+}
+
+func onlinelist(ctx context.Context) ([]playerinfo, error) {
+	res, err := runCommand(ctx, "account onlinelist")
+	if err != nil {
+		glog.Errorf("onlinelist: %v", err)
+		return nil, fmt.Errorf("server unavailable")
+	}
+	if res.fault != "" {
+		glog.Errorf("onlinelist fault: %q", res.fault)
+		return nil, fmt.Errorf("server unavailable")
+	}
+
+	lines := strings.Split(res.result, "\n")
+	header := false
+	var pi []playerinfo
+	for _, line := range lines {
+		switch {
+		case strings.HasPrefix(line, "-="):
+			continue
+		case strings.HasPrefix(line, "-["):
+		default:
+			glog.Warningf("unparseable line %q", line)
+			continue
+		}
+		if !header {
+			header = true
+			continue
+		}
+		if len(line) != 69 {
+			glog.Warningf("wrong line length: %q", line)
+			continue
+		}
+		account := strings.ToLower(strings.TrimSpace(line[2:18]))
+		if line[18:20] != "][" {
+			glog.Warningf("unparseable line %q (wrong sep1)", line)
+			continue
+		}
+		character := strings.TrimSpace(line[20:32])
+		if line[32:34] != "][" {
+			glog.Warningf("unparseable line %q (wrong sep2)", line)
+			continue
+		}
+		pi = append(pi, playerinfo{
+			Account:   account,
+			Character: character,
+		})
+	}
+	glog.Infof("Onlinelist: %v", pi)
+	return pi, nil
+}