// Copyright (C) 2018-2020 CornierKhan1 // // WiiSOAP is SOAP Server Software, designed specifically to handle Wii Shop Channel SOAP. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see http://www.gnu.org/licenses/. package main import ( "encoding/base64" "encoding/xml" "errors" "fmt" "github.com/antchfx/xmlquery" "io" "log" "math/rand" "regexp" "strconv" "strings" "time" ) var namespaceParse = regexp.MustCompile(`^urn:(.{3})\.wsapi\.broadon\.com/(.*)$`) // parseAction interprets contents along the lines of "urn:ecs.wsapi.broadon.com/CheckDeviceStatus", // where "CheckDeviceStatus" is the action to be performed. func parseAction(original string) (string, string) { // Intended to return the original string, the service's name and the name of the action. matches := namespaceParse.FindStringSubmatch(original) if len(matches) != 3 { // It seems like the passed action was not matched properly. return "", "" } service := matches[1] action := matches[2] return service, action } // NewEnvelope returns a new Envelope with proper attributes initialized. func NewEnvelope(service string, action string, body []byte) (*Envelope, error) { // Get a sexy new timestamp to use. timestampNano := fmt.Sprint(time.Now().UTC().UnixNano())[0:13] // Tidy up parsed document for easier usage going forward. doc, err := normalise(service, action, strings.NewReader(string(body))) if err != nil { return nil, err } // Return an envelope with properly set defaults to respond with. e := Envelope{ SOAPEnv: "http://schemas.xmlsoap.org/soap/envelope/", XSD: "http://www.w3.org/2001/XMLSchema", XSI: "http://www.w3.org/2001/XMLSchema-instance", Body: Body{ Response: Response{ XMLName: xml.Name{Local: action + "Response"}, XMLNS: "urn:" + service + ".wsapi.broadon.com", TimeStamp: timestampNano, }, }, doc: doc, } // Obtain common request values. err = e.ObtainCommon() if err != nil { return nil, err } return &e, nil } // Timestamp returns a shared timestamp for this request. func (e *Envelope) Timestamp() string { return e.Body.Response.TimeStamp } // DeviceId returns the Device ID for this request. func (e *Envelope) DeviceId() int { return e.Body.Response.DeviceId } // Region returns the region for this request. It should be only used in IAS-related requests. func (e *Envelope) Region() string { return e.region } // Country returns the region for this request. It should be only used in IAS-related requests. func (e *Envelope) Country() string { return e.country } // Language returns the region for this request. It should be only used in IAS-related requests. func (e *Envelope) Language() string { return e.language } // AccountId returns the account ID for this request. It should be only used in authenticated requests. // If for whatever reason AccountId is not present (such as in SyncRegistration), // it will return an error. Please design in a way so that this is not an issue. func (e *Envelope) AccountId() (int64, error) { accountId, err := e.getKey("AccountId") if err != nil { return 0, nil } return strconv.ParseInt(accountId, 10, 64) } // ObtainCommon interprets a given node, and updates the envelope with common key values. func (e *Envelope) ObtainCommon() error { var err error // These fields are common across all requests. e.Body.Response.Version, err = e.getKey("Version") if err != nil { return err } deviceIdString, err := e.getKey("DeviceId") if err != nil { return err } e.Body.Response.DeviceId, err = strconv.Atoi(deviceIdString) if err != nil { return err } e.Body.Response.MessageId, err = e.getKey("MessageId") if err != nil { return err } // These are as well, but we do not need to send them back in our response. e.region, err = e.getKey("Region") if err != nil { return err } e.country, err = e.getKey("Country") if err != nil { return err } e.language, err = e.getKey("Language") if err != nil { return err } return nil } // AddKVNode adds a given key by name to a specified value, such as value. func (e *Envelope) AddKVNode(key string, value string) { e.Body.Response.CustomFields = append(e.Body.Response.CustomFields, KVField{ XMLName: xml.Name{Local: key}, Value: value, }) } // AddCustomType adds a given key by name to a specified structure. func (e *Envelope) AddCustomType(customType interface{}) { e.Body.Response.CustomFields = append(e.Body.Response.CustomFields, customType) } // becomeXML marshals the Envelope object, returning the intended boolean state on success. // ..there has to be a better way to do this, TODO. func (e *Envelope) becomeXML() (bool, string) { // Non-zero error codes indicate a failure. intendedStatus := e.Body.Response.ErrorCode == 0 var contents []byte var err error // If we're in debug mode, pretty print XML. if isDebug { contents, err = xml.MarshalIndent(e, "", " ") } else { contents, err = xml.Marshal(e) } if err != nil { return false, "an error occurred marshalling XML: " + err.Error() } else { // Add XML header on top of existing contents. result := xml.Header + string(contents) return intendedStatus, result } } // Error sets the necessary keys for this SOAP response to reflect the given error. func (e *Envelope) Error(errorCode int, reason string, err error) { e.Body.Response.ErrorCode = errorCode // Ensure all additional fields are empty to avoid conflict. e.Body.Response.CustomFields = nil e.AddKVNode("ErrorMessage", fmt.Sprintf("%s: %v", reason, err)) } // parseNameValue parses the output of *xmlquery.Node.InnerText when it is a nested Name and Value node. func parseNameValue(s string) (string, string) { s = strings.TrimSpace(s) s = strings.Replace(s, " ", "", strings.Count(s, " ")-1) decoded := strings.Split(s, " ") return strings.TrimSuffix(decoded[0], "\n"), decoded[1] } // normalise parses a document, returning a document with only the request type's child nodes, stripped of prefix. func normalise(service string, action string, reader io.Reader) (*xmlquery.Node, error) { doc, err := xmlquery.Parse(reader) if err != nil { return nil, err } // Find the keys for this element named after the action. result := doc.SelectElement("//" + service + ":" + action) if result == nil { return nil, errors.New("missing root node") } stripNamespace(result) return result, nil } // stripNamespace removes a prefix from nodes, changing a key from "ias:Version" to "Version". // It is based off of https://github.com/antchfx/xmlquery/issues/15#issuecomment-567575075. func stripNamespace(node *xmlquery.Node) { node.Prefix = "" for child := node.FirstChild; child != nil; child = child.NextSibling { stripNamespace(child) } } // getKey returns the value for a child key from a node, if documented. func (e *Envelope) getKey(key string) (string, error) { node := xmlquery.FindOne(e.doc, "//"+key) if node == nil { return "", errors.New("missing mandatory key named " + key) } else { return node.InnerText(), nil } } // getKeys returns a list of xmlquery.Node, if documented. func (e *Envelope) getKeys(key string) ([]*xmlquery.Node, error) { node := xmlquery.Find(e.doc, "//"+key) if node == nil { return nil, errors.New("missing mandatory key named " + key) } else { return node, nil } } // Derived from https://stackoverflow.com/a/31832326, adding numbers const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // RandString generates a random string with n length. func RandString(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } // debugPrint logs a message only if this program is running in debug mode. func debugPrint(v ...interface{}) { if !isDebug { return } log.Print(v...) } // b64 returns a base64-encoded string of the given bytes. func b64(b []byte) string { return base64.StdEncoding.EncodeToString(b) }