Restructure requests to route-based format

Instead of individual handlers, each request deals with one Envelope. This allows fine-tuned error control, along with what's effectively middleware for authentication.
This commit is contained in:
Spotlight 2020-09-05 08:58:01 -05:00
parent e9445110b0
commit 37fb20d8df
No known key found for this signature in database
GPG Key ID: 874AA355B3209BDC
6 changed files with 425 additions and 336 deletions

44
ecs.go
View File

@ -20,7 +20,6 @@ package main
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/antchfx/xmlquery"
"log" "log"
) )
@ -36,16 +35,8 @@ func ecsInitialize() {
} }
} }
func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) { func checkDeviceStatus(e *Envelope) {
// All actions below are for ECS-related functions.
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 "CheckDeviceStatus":
// You need to POST some SOAP from WSC if you wanna get some, honey. ;3 // You need to POST some SOAP from WSC if you wanna get some, honey. ;3
fmt.Println("The request is valid! Responding...")
e.AddCustomType(Balance{ e.AddCustomType(Balance{
Amount: 2018, Amount: 2018,
Currency: "POINTS", Currency: "POINTS",
@ -53,19 +44,18 @@ func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
e.AddKVNode("ForceSyncTime", "0") e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp()) e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp()) e.AddKVNode("SyncTime", e.Timestamp())
break }
case "NotifyETicketsSynced": func notifyETicketsSynced(e *Envelope) {
// This is a disgusting request, but 20 dollars is 20 dollars. ;3 // This is a disgusting request, but 20 dollars is 20 dollars. ;3
}
fmt.Println("The request is valid! Responding...") func listETickets(e *Envelope) {
break
case "ListETickets":
fmt.Println("The request is valid! Responding...") fmt.Println("The request is valid! Responding...")
rows, err := ownedTitles.Query("todo, sorry") rows, err := ownedTitles.Query("todo, sorry")
if err != nil { if err != nil {
return e.ReturnError(2, "that's all you've got for me? ;3", err) e.Error(2, "that's all you've got for me? ;3", err)
return
} }
// Add all available titles for this account. // Add all available titles for this account.
@ -77,7 +67,8 @@ func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
var revocationDate int var revocationDate int
err = rows.Scan(&ticketId, &titleId, &version, &revocationDate) err = rows.Scan(&ticketId, &titleId, &version, &revocationDate)
if err != nil { if err != nil {
return e.ReturnError(2, "that's all you've got for me? ;3", err) e.Error(2, "that's all you've got for me? ;3", err)
return
} }
e.AddCustomType(Tickets{ e.AddCustomType(Tickets{
@ -95,19 +86,17 @@ func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
e.AddKVNode("ForceSyncTime", "0") e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp()) e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp()) e.AddKVNode("SyncTime", e.Timestamp())
break }
case "GetETickets": func getETickets(e *Envelope) {
fmt.Println("The request is valid! Responding...") fmt.Println("The request is valid! Responding...")
e.AddKVNode("ForceSyncTime", "0") e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp()) e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp()) e.AddKVNode("SyncTime", e.Timestamp())
break }
case "PurchaseTitle": func purchaseTitle(e *Envelope) {
// If you wanna fun time, it's gonna cost ya extra sweetie... ;3 // If you wanna fun time, it's gonna cost ya extra sweetie... ;3
fmt.Println("The request is valid! Responding...")
e.AddCustomType(Balance{ e.AddCustomType(Balance{
Amount: 2018, Amount: 2018,
Currency: "POINTS", Currency: "POINTS",
@ -121,11 +110,4 @@ func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
e.AddKVNode("Certs", "00000000") e.AddKVNode("Certs", "00000000")
e.AddKVNode("TitleId", "00000000") e.AddKVNode("TitleId", "00000000")
e.AddKVNode("ETickets", "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()
} }

108
ias.go
View File

@ -19,12 +19,11 @@ package main
import ( import (
"crypto/md5" "crypto/md5"
sha2562 "crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"github.com/RiiConnect24/wiino/golang" wiino "github.com/RiiConnect24/wiino/golang"
"github.com/antchfx/xmlquery"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
"log" "log"
"math/rand" "math/rand"
@ -41,97 +40,82 @@ func iasInitialize() {
} }
} }
func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) { func checkRegistration(e *Envelope) {
// All IAS-related functions should contain these keys. serialNo, err := getKey(e.doc, "SerialNumber")
region, err := getKey(doc, "Region")
if err != nil { if err != nil {
return e.ReturnError(5, "not good enough for me. ;3", err) e.Error(5, "not good enough for me. ;3", err)
} return
country, err := getKey(doc, "Country")
if err != nil {
return e.ReturnError(5, "not good enough for me. ;3", err)
}
language, err := getKey(doc, "Language")
if err != nil {
return e.ReturnError(5, "not good enough for me. ;3", err)
} }
// All actions below are for IAS-related functions.
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 e.ReturnError(5, "not good enough for me. ;3", err)
}
fmt.Println("The request is valid! Responding...")
e.AddKVNode("OriginalSerialNumber", serialNo) e.AddKVNode("OriginalSerialNumber", serialNo)
e.AddKVNode("DeviceStatus", "R") e.AddKVNode("DeviceStatus", "R")
break }
case "GetChallenge": func getChallenge(e *Envelope) {
fmt.Println("The request is valid! Responding...")
// The official Wii Shop Channel requests a Challenge from the server, and promptly disregards it. // The official Wii Shop Channel requests a Challenge from the server, and promptly disregards it.
// (Sometimes, it may not request a challenge at all.) No attempt is made to validate the response. // (Sometimes, it may not request a challenge at all.) No attempt is made to validate the response.
// It then uses another hard-coded value in place of this returned value entirely in any situation. // It then uses another hard-coded value in place of this returned value entirely in any situation.
// For this reason, we consider it irrelevant. // For this reason, we consider it irrelevant.
e.AddKVNode("Challenge", SharedChallenge) e.AddKVNode("Challenge", SharedChallenge)
break
case "GetRegistrationInfo": }
func getRegistrationInfo(e *Envelope) {
reason := "how dirty. ;3" reason := "how dirty. ;3"
accountId, err := getKey(doc, "AccountId") accountId, err := getKey(e.doc, "AccountId")
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
} }
deviceCode, err := getKey(doc, "DeviceCode") deviceCode, err := getKey(e.doc, "DeviceCode")
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
} }
fmt.Println("The request is valid! Responding...")
e.AddKVNode("AccountId", accountId) e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", "00000000") e.AddKVNode("DeviceToken", "00000000")
e.AddKVNode("DeviceTokenExpired", "false") e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", country) e.AddKVNode("Country", e.Country())
e.AddKVNode("ExtAccountId", "") e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode) e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("DeviceStatus", "R") e.AddKVNode("DeviceStatus", "R")
// This _must_ be POINTS. // This _must_ be POINTS.
e.AddKVNode("Currency", "POINTS") e.AddKVNode("Currency", "POINTS")
break }
case "Register": func register(e *Envelope) {
reason := "disgustingly invalid. ;3" reason := "disgustingly invalid. ;3"
deviceCode, err := getKey(doc, "DeviceCode") deviceCode, err := getKey(e.doc, "DeviceCode")
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
return
} }
registerRegion, err := getKey(doc, "RegisterRegion") registerRegion, err := getKey(e.doc, "RegisterRegion")
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
return
} }
if registerRegion != region { if registerRegion != e.Region() {
return e.ReturnError(7, reason, errors.New("region does not match registration region")) e.Error(7, reason, errors.New("region does not match registration region"))
return
} }
serialNo, err := getKey(doc, "SerialNumber") serialNo, err := getKey(e.doc, "SerialNumber")
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
return
} }
// Validate given friend code. // Validate given friend code.
userId, err := strconv.ParseUint(deviceCode, 10, 64) userId, err := strconv.ParseUint(deviceCode, 10, 64)
if err != nil { if err != nil {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
return
} }
if wiino.NWC24CheckUserID(userId) != 0 { if wiino.NWC24CheckUserID(userId) != 0 {
return e.ReturnError(7, reason, err) e.Error(7, reason, err)
return
} }
// Generate a random 9-digit number, padding zeros as necessary. // Generate a random 9-digit number, padding zeros as necessary.
@ -146,40 +130,34 @@ func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
// We'll store this in our database, as storing the md5 itself is effectively the token. // We'll store this in our database, as storing the md5 itself is effectively the token.
// It would not be good for security to directly store the token either. // It would not be good for security to directly store the token either.
// This is the hash of the md5 represented as a string, not individual byte values. // This is the hash of the md5 represented as a string, not individual byte values.
doublyHashedDeviceToken := fmt.Sprintf("%x", sha2562.Sum256([]byte(md5DeviceToken))) doublyHashedDeviceToken := fmt.Sprintf("%x", sha256.Sum256([]byte(md5DeviceToken)))
// Insert all of our obtained values to the database.. // Insert all of our obtained values to the database..
_, err = registerUser.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, region, country, language, serialNo, deviceCode) _, err = registerUser.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, e.Region(), e.Country(), e.Language(), serialNo, deviceCode)
if err != nil { if err != nil {
// It's okay if this isn't a MySQL error, as perhaps other issues have come in. // It's okay if this isn't a MySQL error, as perhaps other issues have come in.
if driverErr, ok := err.(*mysql.MySQLError); ok { if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == 1062 { if driverErr.Number == 1062 {
return e.ReturnError(7, reason, errors.New("user already exists")) e.Error(7, reason, errors.New("user already exists"))
return
} }
} }
log.Printf("error executing statement: %v\n", err) log.Printf("error executing statement: %v\n", err)
return e.ReturnError(7, reason, errors.New("failed to execute db operation")) e.Error(7, reason, errors.New("failed to execute db operation"))
return
} }
fmt.Println("The request is valid! Responding...") fmt.Println("The request is valid! Responding...")
e.AddKVNode("AccountId", accountId) e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", deviceToken) e.AddKVNode("DeviceToken", deviceToken)
e.AddKVNode("DeviceTokenExpired", "false") e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", country) e.AddKVNode("Country", e.Country())
// Optionally, one can send back DeviceCode and ExtAccountId to update on device. // Optionally, one can send back DeviceCode and ExtAccountId to update on device.
// We send these back as-is regardless. // We send these back as-is regardless.
e.AddKVNode("ExtAccountId", "") e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode) e.AddKVNode("DeviceCode", deviceCode)
break }
case "Unregister": func unregister(e *Envelope) {
// how abnormal... ;3 // how abnormal... ;3
fmt.Println("The request is valid! Responding...")
break
default:
return false, "WiiSOAP can't handle this. Try again later or actually use a Wii instead of a computer."
}
return e.ReturnSuccess()
} }

94
main.go
View File

@ -25,7 +25,6 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"strings"
"time" "time"
) )
@ -76,79 +75,26 @@ func main() {
// Start the HTTP server. // Start the HTTP server.
fmt.Printf("Starting HTTP connection (%s)...\nNot using the usual port for HTTP?\nBe sure to use a proxy, otherwise the Wii can't connect!\n", CON.Address) fmt.Printf("Starting HTTP connection (%s)...\nNot using the usual port for HTTP?\nBe sure to use a proxy, otherwise the Wii can't connect!\n", CON.Address)
// These following endpoints don't have to match what the official WSC have. r := NewRoute()
// However, semantically, it feels proper. ecs := r.HandleGroup("ecs")
http.HandleFunc("/ecs/services/ECommerceSOAP", commonHandler) {
http.HandleFunc("/ias/services/IdentityAuthenticationSOAP", commonHandler) ecs.Authenticated("CheckDeviceStatus", checkDeviceStatus)
log.Fatal(http.ListenAndServe(CON.Address, nil)) ecs.Authenticated("NotifyETicketsSynced", notifyETicketsSynced)
ecs.Authenticated("ListETickets", listETickets)
ecs.Authenticated("GetETickets", getETickets)
ecs.Authenticated("PurchaseTitle", purchaseTitle)
}
ias := r.HandleGroup("ias")
{
ias.Unauthenticated("CheckRegistration", checkRegistration)
ias.Unauthenticated("GetChallenge", getChallenge)
ias.Authenticated("GetRegistrationInfo", getRegistrationInfo)
ias.Unauthenticated("Register", register)
ias.Authenticated("Unregister", unregister)
}
log.Fatal(http.ListenAndServe(CON.Address, r.Handle()))
// From here on out, all special cool things should go into their respective handler function. // From here on out, all special cool things should go into their respective handler function.
} }
func commonHandler(w http.ResponseWriter, r *http.Request) {
// Figure out the action to handle via header.
service, action := parseAction(r.Header.Get("SOAPAction"))
if service == "" || action == "" {
printError(w, "WiiSOAP can't handle this. Try again later or actually use a Wii instead of a computer.")
return
}
// Verify this is a service type we know.
switch service {
case "ecs":
case "ias":
break
default:
printError(w, "Unsupported service type...")
return
}
fmt.Println("[!] Incoming " + strings.ToUpper(service) + " request - handling for " + action)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
printError(w, "Error reading request body...")
return
}
// Tidy up parsed document for easier usage going forward.
doc, err := normalise(service, action, strings.NewReader(string(body)))
if err != nil {
printError(w, "Error interpreting request body: "+err.Error())
return
}
fmt.Println("Received:", string(body))
// Insert the current action being performed.
envelope := NewEnvelope(service, action)
// Extract shared values from this request.
err = envelope.ObtainCommon(doc)
if err != nil {
printError(w, "Error handling request body: "+err.Error())
return
}
var successful bool
var result string
if service == "ias" {
successful, result = iasHandler(envelope, doc)
} else if service == "ecs" {
successful, result = ecsHandler(envelope, doc)
}
if successful {
// Write returned with proper Content-Type
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
w.Write([]byte(result))
} else {
printError(w, result)
}
fmt.Println("[!] End of " + strings.ToUpper(service) + " Request.\n")
}
func printError(w http.ResponseWriter, reason string) {
http.Error(w, reason, http.StatusInternalServerError)
fmt.Println("Failed to handle request: " + reason)
}

141
route.go Normal file
View File

@ -0,0 +1,141 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
// Route defines a header to be checked for actions, and an array of actions to handle.
type Route struct {
HeaderName string
Actions []Action
}
// Action contains information about how a specified action should be handled.
type Action struct {
ActionName string
Callback func(e *Envelope)
NeedsAuthentication bool
ServiceType string
}
// NewRoute produces a new route struct with appropriate header defaults.
func NewRoute() Route {
return Route{
HeaderName: "SOAPAction",
}
}
// RoutingGroup defines a group of actions for a given service type.
type RoutingGroup struct {
Route *Route
ServiceType string
}
// HandleGroup returns a routing group type for the given service type.
func (r *Route) HandleGroup(serviceType string) RoutingGroup {
return RoutingGroup{
Route: r,
ServiceType: serviceType,
}
}
// Unauthenticated associates an action to a function to be handled without authentication.
func (r *RoutingGroup) Unauthenticated(action string, function func(e *Envelope)) {
r.Route.Actions = append(r.Route.Actions, Action{
ActionName: action,
Callback: function,
NeedsAuthentication: false,
ServiceType: r.ServiceType,
})
}
// Authenticated associates an action to a function to be handled with authentication.
func (r *RoutingGroup) Authenticated(action string, function func(e *Envelope)) {
r.Route.Actions = append(r.Route.Actions, Action{
ActionName: action,
Callback: function,
NeedsAuthentication: true,
ServiceType: r.ServiceType,
})
}
func (route *Route) Handle() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s via %s", r.Method, r.URL, r.Host)
// Check if there's a header of the type we need.
service, actionName := parseAction(r.Header.Get("SOAPAction"))
if service == "" || actionName == "" || r.Method != "POST" {
printError(w, "WiiSOAP can't handle this. Try again later.")
return
}
// Verify this is a service type we know.
switch service {
case "ecs":
case "ias":
break
default:
printError(w, "Unsupported service type...")
return
}
fmt.Println("[!] Incoming " + strings.ToUpper(service) + " request - handling for " + actionName)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
printError(w, "Error reading request body...")
return
}
// Ensure we can route to this action before processing.
// Search all registered actions and find a matching action.
var action Action
for _, routeAction := range route.Actions {
if routeAction.ActionName == actionName && routeAction.ServiceType == service {
action = routeAction
}
}
// Action is only properly populated if we found it previously.
if action.ActionName == "" && action.ServiceType == "" {
printError(w, "WiiSOAP can't handle this. Try again later.")
return
}
fmt.Println("Received:", string(body))
// Insert the current action being performed.
e, err := NewEnvelope(service, actionName, body)
if err != nil {
printError(w, "Error interpreting request body: "+err.Error())
return
}
// IAS-specific actions can have separate values available.
// TODO: AUTH
// TODO: QUITE A LOT
// Call this action.
action.Callback(e)
// The action has now finished its task, and we can serialize.
// Output may or may not truly be XML depending on where things failed.
// We'll expect the best, however.
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
success, contents := e.becomeXML()
if !success {
// This is not what we wanted, and we need to reflect that.
w.WriteHeader(http.StatusInternalServerError)
}
w.Write([]byte(contents))
})
}
func printError(w http.ResponseWriter, reason string) {
http.Error(w, reason, http.StatusInternalServerError)
fmt.Println("Failed to handle request: " + reason)
}

View File

@ -17,7 +17,10 @@
package main package main
import "encoding/xml" import (
"encoding/xml"
"github.com/antchfx/xmlquery"
)
///////////////////// /////////////////////
// SOAP STRUCTURES // // SOAP STRUCTURES //
@ -47,7 +50,12 @@ type Envelope struct {
Body Body Body Body
// Used for internal state tracking. // Used for internal state tracking.
action string doc *xmlquery.Node
// Common IAS values.
region string
country string
language string
} }
// Body represents the nested soapenv:Body element as a child on the root element, // Body represents the nested soapenv:Body element as a child on the root element,
@ -86,7 +94,7 @@ type KVField struct {
type Balance struct { type Balance struct {
XMLName xml.Name `xml:"Balance"` XMLName xml.Name `xml:"Balance"`
Amount int `xml:"Amount"` Amount int `xml:"Amount"`
Currency string `xml"Currency"` Currency string `xml:"Currency"`
} }
// Transactions represents a common XML structure. // Transactions represents a common XML structure.

View File

@ -25,6 +25,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"regexp" "regexp"
"strings"
"time" "time"
) )
@ -46,11 +47,18 @@ func parseAction(original string) (string, string) {
} }
// NewEnvelope returns a new Envelope with proper attributes initialized. // NewEnvelope returns a new Envelope with proper attributes initialized.
func NewEnvelope(service string, action string) Envelope { func NewEnvelope(service string, action string, body []byte) (*Envelope, error) {
// Get a sexy new timestamp to use. // Get a sexy new timestamp to use.
timestampNano := fmt.Sprint(time.Now().UTC().UnixNano())[0:13] timestampNano := fmt.Sprint(time.Now().UTC().UnixNano())[0:13]
return Envelope{ // 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/", SOAPEnv: "http://schemas.xmlsoap.org/soap/envelope/",
XSD: "http://www.w3.org/2001/XMLSchema", XSD: "http://www.w3.org/2001/XMLSchema",
XSI: "http://www.w3.org/2001/XMLSchema-instance", XSI: "http://www.w3.org/2001/XMLSchema-instance",
@ -62,13 +70,16 @@ func NewEnvelope(service string, action string) Envelope {
TimeStamp: timestampNano, TimeStamp: timestampNano,
}, },
}, },
action: action, doc: doc,
}
} }
// Action returns the action for this service. // Obtain common request values.
func (e *Envelope) Action() string { err = e.ObtainCommon()
return e.action if err != nil {
return nil, err
}
return &e, nil
} }
// Timestamp returns a shared timestamp for this request. // Timestamp returns a shared timestamp for this request.
@ -81,9 +92,25 @@ func (e *Envelope) DeviceId() string {
return e.Body.Response.DeviceId 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
}
// ObtainCommon interprets a given node, and updates the envelope with common key values. // ObtainCommon interprets a given node, and updates the envelope with common key values.
func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error { func (e *Envelope) ObtainCommon() error {
var err error var err error
doc := e.doc
// These fields are common across all requests. // These fields are common across all requests.
e.Body.Response.Version, err = getKey(doc, "Version") e.Body.Response.Version, err = getKey(doc, "Version")
@ -99,6 +126,20 @@ func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error {
return err return err
} }
// These are as well, but we do not need to send them back in our response.
e.region, err = getKey(doc, "Region")
if err != nil {
return err
}
e.country, err = getKey(doc, "Country")
if err != nil {
return err
}
e.language, err = getKey(doc, "Language")
if err != nil {
return err
}
return nil return nil
} }
@ -117,7 +158,10 @@ func (e *Envelope) AddCustomType(customType interface{}) {
// becomeXML marshals the Envelope object, returning the intended boolean state on success. // becomeXML marshals the Envelope object, returning the intended boolean state on success.
// ..there has to be a better way to do this, TODO. // ..there has to be a better way to do this, TODO.
func (e *Envelope) becomeXML(intendedStatus bool) (bool, string) { func (e *Envelope) becomeXML() (bool, string) {
// Non-zero error codes indicate a failure.
intendedStatus := e.Body.Response.ErrorCode == 0
contents, err := xml.Marshal(e) contents, err := xml.Marshal(e)
if err != nil { if err != nil {
return false, "an error occurred marshalling XML: " + err.Error() return false, "an error occurred marshalling XML: " + err.Error()
@ -128,16 +172,8 @@ func (e *Envelope) becomeXML(intendedStatus bool) (bool, string) {
} }
} }
// ReturnSuccess returns a standard SOAP response with a positive error code. // Error sets the necessary keys for this SOAP response to reflect the given error.
func (e *Envelope) ReturnSuccess() (bool, string) { func (e *Envelope) Error(errorCode int, reason string, err error) {
// Ensure the error code is 0.
e.Body.Response.ErrorCode = 0
return e.becomeXML(true)
}
// ReturnError returns a standard SOAP response with an error code.
func (e *Envelope) ReturnError(errorCode int, reason string, err error) (bool, string) {
e.Body.Response.ErrorCode = errorCode e.Body.Response.ErrorCode = errorCode
// Ensure all additional fields are empty to avoid conflict. // Ensure all additional fields are empty to avoid conflict.
@ -145,8 +181,6 @@ func (e *Envelope) ReturnError(errorCode int, reason string, err error) (bool, s
e.AddKVNode("UserReason", reason) e.AddKVNode("UserReason", reason)
e.AddKVNode("ServerReason", err.Error()) 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. // normalise parses a document, returning a document with only the request type's child nodes, stripped of prefix.