| 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 |
| } |