// 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 ( "errors" "fmt" "github.com/antchfx/xmlquery" "io" "regexp" "strconv" "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 } // formatHeader formats a response type and the proper service. func formatHeader(responseType string, service string) string { return fmt.Sprintf(Header, responseType, "urn:"+service+".wsapi.broadon.com") } // formatTemplate inserts common, cross-requests values into every request. func formatTemplate(common map[string]string, errorCode int) string { return fmt.Sprintf(Template, common["Version"], common["DeviceID"], common["MessageId"], common["Timestamp"], errorCode) } // formatFooter formats the closing tags of any SOAP request per previous response type. func formatFooter(responseType string) string { return fmt.Sprintf(Footer, responseType) } // formatForNamespace mangles together several variables throughout a SOAP request. func formatForNamespace(common map[string]string, errorCode int, extraContents string) string { return fmt.Sprintf("%s%s%s%s", formatHeader(common["Action"], common["Service"]), formatTemplate(common, errorCode), "\t"+extraContents+"\n", formatFooter(common["Action"]), ) } // formatSuccess returns a standard SOAP response with a positive error code, and additional contents. func formatSuccess(common map[string]string, extraContents string) (bool, string) { return true, formatForNamespace(common, 0, extraContents) } // formatError returns a standard SOAP response with an error code. func formatError(common map[string]string, errorCode int, reason string, err error) (bool, string) { extra := "" + reason + "\n" + "\t" + err.Error() + "\n" return false, formatForNamespace(common, errorCode, extra) } // 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) } } // obtainCommon interprets a given node, and from its children finds common keys and respective values across all requests. func obtainCommon(doc *xmlquery.Node) (map[string]string, error) { info := make(map[string]string) // Get a sexy new timestamp to use. timestampNano := strconv.FormatInt(time.Now().UTC().Unix(), 10) info["Timestamp"] = timestampNano + "000" // These fields are common across all requests. // Looping through all... shared := []string{"Version", "DeviceId", "MessageId"} for _, key := range shared { // select their node by name... value, err := getKey(doc, key) if err != nil { return nil, err } // and insert their value to our map. info[key] = value } return info, nil } // getKey returns the value for a child key from a node, if documented. func getKey(doc *xmlquery.Node, key string) (string, error) { node := xmlquery.FindOne(doc, "//"+key) if node == nil { return "", errors.New("missing mandatory key named " + key) } else { return node.InnerText(), nil } }