From f32e2a5f2e5b28083f7d2455d0f2be135d2dfcc9 Mon Sep 17 00:00:00 2001 From: Spotlight Date: Tue, 18 Aug 2020 05:35:53 -0500 Subject: [PATCH] Convert sprintf-based XML handling to struct-based marshalling This allows the opportunity to have various handler methods for inserting XML data, instead of a jungle between properly parsing and string-based prayer. --- ecs.go | 63 ++++++++++++----------- ias.go | 60 ++++++++++++---------- main.go | 41 ++++----------- structure.go | 63 +++++++++++++++++++++++ utils.go | 138 ++++++++++++++++++++++++++++++++------------------- 5 files changed, 225 insertions(+), 140 deletions(-) diff --git a/ecs.go b/ecs.go index fbd5680..b69c792 100644 --- a/ecs.go +++ b/ecs.go @@ -22,11 +22,9 @@ import ( "github.com/antchfx/xmlquery" ) -func ecsHandler(common map[string]string, doc *xmlquery.Node) (bool, string) { - timestamp := common["Timestamp"] - +func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) { // All actions below are for ECS-related functions. - switch common["Action"] { + switch e.Action() { // TODO: Make the case functions cleaner. (e.g. Should the response be a variable?) // TODO: Update the responses so that they query the SQL Database for the proper information (e.g. Device Code, Token, etc). @@ -34,51 +32,52 @@ func ecsHandler(common map[string]string, doc *xmlquery.Node) (bool, string) { //You need to POST some SOAP from WSC if you wanna get some, honey. ;3 fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(` - 2018 - POINTS - - 0 - %s - %s`, timestamp, timestamp) - return formatSuccess(common, custom) + e.AddCustomType(Balance{ + Amount: 2018, + Currency: "POINTS", + }) + e.AddKVNode("ForceSyncTime", "0") + e.AddKVNode("ExtTicketTime", e.Timestamp()) + e.AddKVNode("SyncTime", e.Timestamp()) + break case "NotifyETicketsSynced": // This is a disgusting request, but 20 dollars is 20 dollars. ;3 fmt.Println("The request is valid! Responding...") - return formatSuccess(common, "") + break case "ListETickets": // that's all you've got for me? ;3 fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(`0 - %s - %s`, timestamp, timestamp) - return formatSuccess(common, custom) + e.AddKVNode("ForceSyncTime", "0") + e.AddKVNode("ExtTicketTime", e.Timestamp()) + e.AddKVNode("SyncTime", e.Timestamp()) + break case "PurchaseTitle": // If you wanna fun time, it's gonna cost ya extra sweetie... ;3 fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(` - 2018 - POINTS - - - 00000000 - %s - PURCHGAME - - %s - 00000000 - 00000000 - 00000000 - 00000000`, timestamp, timestamp) - return formatSuccess(common, custom) + e.AddCustomType(Balance{ + Amount: 2018, + Currency: "POINTS", + }) + e.AddCustomType(Transactions{ + TransactionId: "00000000", + Date: e.Timestamp(), + Type: "PURCHGAME", + }) + e.AddKVNode("SyncTime", e.Timestamp()) + e.AddKVNode("Certs", "00000000") + e.AddKVNode("TitleId", "00000000") + e.AddKVNode("ETickets", "00000000") + break default: return false, "WiiSOAP can't handle this. Try again later or actually use a Wii instead of a computer." } + + return e.ReturnSuccess() } diff --git a/ias.go b/ias.go index f813e8d..850eaa6 100644 --- a/ias.go +++ b/ias.go @@ -22,72 +22,78 @@ import ( "github.com/antchfx/xmlquery" ) -func iasHandler(common map[string]string, doc *xmlquery.Node) (bool, string) { +func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) { // All actions below are for IAS-related functions. - switch common["Action"] { + switch e.Action() { // TODO: Make the case functions cleaner. (e.g. Should the response be a variable?) // TODO: Update the responses so that they query the SQL Database for the proper information (e.g. Device Code, Token, etc). case "CheckRegistration": serialNo, err := getKey(doc, "SerialNumber") if err != nil { - return formatError(common, 5, "not good enough for me. ;3", err) + return e.ReturnError(5, "not good enough for me. ;3", err) } fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(`%s - R`, serialNo) - return formatSuccess(common, custom) + e.AddKVNode("OriginalSerialNumber", serialNo) + e.AddKVNode("DeviceStatus", "R") + break + + case "GetChallenge": + fmt.Println("The request is valid! Responding...") + break case "GetRegistrationInfo": accountId, err := getKey(doc, "AccountId") if err != nil { - return formatError(common, 6, "how dirty. ;3", err) + return e.ReturnError(7, "how dirty. ;3", err) } country, err := getKey(doc, "Country") if err != nil { - return formatError(common, 6, "how dirty. ;3", err) + return e.ReturnError(7, "how dirty. ;3", err) } fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(`%s - 00000000 - false - %s - - 0000000000000000 - R - POINTS`, accountId, country) - return formatSuccess(common, custom) + e.AddKVNode("AccountId", accountId) + e.AddKVNode("DeviceToken", "00000000") + e.AddKVNode("DeviceTokenExpired", "false") + e.AddKVNode("Country", country) + e.AddKVNode("ExtAccountId", "") + e.AddKVNode("DeviceCode", "0000000000000000") + e.AddKVNode("DeviceStatus", "R") + // This _must_ be POINTS. + e.AddKVNode("Currency", "POINTS") + break case "Register": accountId, err := getKey(doc, "AccountId") if err != nil { - return formatError(common, 7, "disgustingly invalid. ;3", err) + return e.ReturnError(8, "disgustingly invalid. ;3", err) } country, err := getKey(doc, "Country") if err != nil { - return formatError(common, 7, "disgustingly invalid. ;3", err) + return e.ReturnError(8, "disgustingly invalid. ;3", err) } fmt.Println("The request is valid! Responding...") - custom := fmt.Sprintf(`%s - 00000000 - %s - - 00000000`, accountId, country) - return formatSuccess(common, custom) + e.AddKVNode("AccountId", accountId) + e.AddKVNode("DeviceToken", "00000000") + e.AddKVNode("Country", country) + e.AddKVNode("ExtAccountId", "") + e.AddKVNode("DeviceCode", "0000000000000000") + break case "Unregister": // how abnormal... ;3 - fmt.Println("The request is valid! Responding...") - return formatSuccess(common, "") + break default: return false, "WiiSOAP can't handle this. Try again later or actually use a Wii instead of a computer." } + + return e.ReturnSuccess() } diff --git a/main.go b/main.go index 03872f0..a025992 100644 --- a/main.go +++ b/main.go @@ -28,28 +28,6 @@ import ( "strings" ) -const ( - // Header is the base format of a SOAP response with string substitutions available. - // All XML constants must be treated as temporary until a proper XPath solution is investigated. - Header = ` - - -<%sResponse xmlns="%s">` + "\n" - // Template describes common fields across all requests, for easy replication. - Template = ` %s - %s - %s - %s - %d - false` + "\n" - // Footer is the base format of a closing envelope in SOAP. - Footer = ` - -` -) - // checkError makes error handling not as ugly and inefficient. func checkError(err error) { if err != nil { @@ -123,23 +101,24 @@ func commonHandler(w http.ResponseWriter, r *http.Request) { return } + fmt.Println("Received:", string(body)) + + // Insert the current action being performed. + envelope := NewEnvelope(service, action) + // Extract shared values from this request. - common, err := obtainCommon(doc) + err = envelope.ObtainCommon(doc) if err != nil { printError(w, "Error handling request body: "+err.Error()) return } - // Insert the current action being performed. - common["Service"] = service - common["Action"] = action - - var result string var successful bool + var result string if service == "ias" { - successful, result = iasHandler(common, doc) + successful, result = iasHandler(envelope, doc) } else if service == "ecs" { - successful, result = ecsHandler(common, doc) + successful, result = ecsHandler(envelope, doc) } if successful { @@ -155,5 +134,5 @@ func commonHandler(w http.ResponseWriter, r *http.Request) { func printError(w http.ResponseWriter, reason string) { http.Error(w, reason, http.StatusInternalServerError) - log.Println("Failed to handle request: " + reason) + fmt.Println("Failed to handle request: " + reason) } diff --git a/structure.go b/structure.go index 9e88b6c..fefcb08 100644 --- a/structure.go +++ b/structure.go @@ -35,3 +35,66 @@ type Config struct { SQLPass string `xml:"SQLPass"` SQLDB string `xml:"SQLDB"` } + +// Envelope represents the root element of any response, soapenv:Envelope. +type Envelope struct { + XMLName string `xml:"soapenv:Envelope"` + SOAPEnv string `xml:"xmlns:soapenv,attr"` + XSD string `xml:"xmlns:xsd,attr"` + XSI string `xml:"xmlns:xsi,attr"` + + // Represents a soapenv:Body within. + Body Body + + // Used for internal state tracking. + action string +} + +// Body represents the nested soapenv:Body element as a child on the root element, +// containing the response intended for the action being handled. +type Body struct { + XMLName string `xml:"soapenv:Body"` + + // Represents the actual response inside + Response Response +} + +// Response describes the inner response format, along with common fields across requests. +type Response struct { + XMLName xml.Name + XMLNS string `xml:"xmlns,attr"` + + // These common fields are persistent across all requests. + Version string `xml:"Version"` + DeviceId string `xml:"DeviceId"` + MessageId string `xml:"MessageId"` + TimeStamp string `xml:"TimeStamp"` + ErrorCode int + ServiceStandbyMode bool `xml:"ServiceStandbyMode"` + + // Allows a simple value node to be inserted. + KVFields []KVField + // Allows for [dynamic content] situations. + CustomFields []interface{} +} + +// KVField represents an individual node in form of Contents. +type KVField struct { + XMLName xml.Name + Value string `xml:",chardata"` +} + +// Balance represents a common XML structure. +type Balance struct { + XMLName xml.Name `xml:"Balance"` + Amount int `xml:"Amount"` + Currency string `xml"Currency"` +} + +// Transactions represents a common XML structure. +type Transactions struct { + XMLName xml.Name `xml:"Transactions"` + TransactionId string `xml:"TransactionId"` + Date string `xml:"Date"` + Type string `xml:"Type"` +} diff --git a/utils.go b/utils.go index b0b944c..c2552bc 100644 --- a/utils.go +++ b/utils.go @@ -18,8 +18,8 @@ package main import ( + "encoding/xml" "errors" - "fmt" "github.com/antchfx/xmlquery" "io" "regexp" @@ -44,41 +44,104 @@ func parseAction(original string) (string, string) { 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") +// NewEnvelope returns a new Envelope with proper attributes initialized. +func NewEnvelope(service string, action string) Envelope { + // Get a sexy new timestamp to use. + timestampNano := strconv.FormatInt(time.Now().UTC().Unix(), 10) + "000" + + return 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, + }, + }, + action: action, + } } -// 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) +// Action returns the action for this service. +func (e *Envelope) Action() string { + return e.action } -// formatFooter formats the closing tags of any SOAP request per previous response type. -func formatFooter(responseType string) string { - return fmt.Sprintf(Footer, responseType) +// Timestamp returns a shared timestamp for this request. +func (e *Envelope) Timestamp() string { + return e.Body.Response.TimeStamp } -// 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"]), - ) +// obtainCommon interprets a given node, and updates the envelope with common key values. +func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error { + var err error + + // These fields are common across all requests. + e.Body.Response.Version, err = getKey(doc, "Version") + if err != nil { + return err + } + e.Body.Response.DeviceId, err = getKey(doc, "DeviceId") + if err != nil { + return err + } + e.Body.Response.MessageId, err = getKey(doc, "MessageId") + if err != nil { + return err + } + + return nil } -// 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) +// AddKVNode adds a given key by name to a specified value. +func (e *Envelope) AddKVNode(key string, value string) { + e.Body.Response.KVFields = append(e.Body.Response.KVFields, 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(intendedStatus bool) (bool, string) { + 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 + } +} + +// ReturnSuccess returns a standard SOAP response with a positive error code. +func (e *Envelope) ReturnSuccess() (bool, string) { + // Ensure the error code is 0. + e.Body.Response.ErrorCode = 0 + + return e.becomeXML(true) } // 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) +func (e *Envelope) ReturnError(errorCode int, reason string, err error) (bool, string) { + e.Body.Response.ErrorCode = errorCode + + // Ensure all additional fields are empty to avoid conflict. + e.Body.Response.KVFields = []KVField{} + e.Body.Response.CustomFields = nil + + e.AddKVNode("UserReason", reason) + e.AddKVNode("ServerReason", err.Error()) + + return e.becomeXML(false) } // normalise parses a document, returning a document with only the request type's child nodes, stripped of prefix. @@ -108,31 +171,6 @@ func stripNamespace(node *xmlquery.Node) { } } -// 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)