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

160
ecs.go
View File

@ -20,7 +20,6 @@ package main
import (
"database/sql"
"fmt"
"github.com/antchfx/xmlquery"
"log"
)
@ -36,96 +35,79 @@ func ecsInitialize() {
}
}
func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
// 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).
func checkDeviceStatus(e *Envelope) {
// You need to POST some SOAP from WSC if you wanna get some, honey. ;3
e.AddCustomType(Balance{
Amount: 2018,
Currency: "POINTS",
})
e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp())
}
case "CheckDeviceStatus":
//You need to POST some SOAP from WSC if you wanna get some, honey. ;3
func notifyETicketsSynced(e *Envelope) {
// This is a disgusting request, but 20 dollars is 20 dollars. ;3
}
fmt.Println("The request is valid! Responding...")
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...")
break
case "ListETickets":
fmt.Println("The request is valid! Responding...")
rows, err := ownedTitles.Query("todo, sorry")
if err != nil {
return e.ReturnError(2, "that's all you've got for me? ;3", err)
}
// Add all available titles for this account.
defer rows.Close()
for rows.Next() {
var ticketId string
var titleId string
var version int
var revocationDate int
err = rows.Scan(&ticketId, &titleId, &version, &revocationDate)
if err != nil {
return e.ReturnError(2, "that's all you've got for me? ;3", err)
}
e.AddCustomType(Tickets{
TicketId: ticketId,
TitleId: titleId,
Version: version,
RevokeDate: revocationDate,
// We do not support migration.
MigrateCount: 0,
MigrateLimit: 0,
})
}
e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp())
break
case "GetETickets":
fmt.Println("The request is valid! Responding...")
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...")
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."
func listETickets(e *Envelope) {
fmt.Println("The request is valid! Responding...")
rows, err := ownedTitles.Query("todo, sorry")
if err != nil {
e.Error(2, "that's all you've got for me? ;3", err)
return
}
return e.ReturnSuccess()
// Add all available titles for this account.
defer rows.Close()
for rows.Next() {
var ticketId string
var titleId string
var version int
var revocationDate int
err = rows.Scan(&ticketId, &titleId, &version, &revocationDate)
if err != nil {
e.Error(2, "that's all you've got for me? ;3", err)
return
}
e.AddCustomType(Tickets{
TicketId: ticketId,
TitleId: titleId,
Version: version,
RevokeDate: revocationDate,
// We do not support migration.
MigrateCount: 0,
MigrateLimit: 0,
})
}
e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp())
}
func getETickets(e *Envelope) {
fmt.Println("The request is valid! Responding...")
e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp())
}
func purchaseTitle(e *Envelope) {
// If you wanna fun time, it's gonna cost ya extra sweetie... ;3
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")
}

260
ias.go
View File

@ -19,12 +19,11 @@ package main
import (
"crypto/md5"
sha2562 "crypto/sha256"
"crypto/sha256"
"database/sql"
"errors"
"fmt"
"github.com/RiiConnect24/wiino/golang"
"github.com/antchfx/xmlquery"
wiino "github.com/RiiConnect24/wiino/golang"
"github.com/go-sql-driver/mysql"
"log"
"math/rand"
@ -41,145 +40,124 @@ func iasInitialize() {
}
}
func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) {
// All IAS-related functions should contain these keys.
region, err := getKey(doc, "Region")
func checkRegistration(e *Envelope) {
serialNo, err := getKey(e.doc, "SerialNumber")
if err != nil {
return e.ReturnError(5, "not good enough for me. ;3", err)
}
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)
e.Error(5, "not good enough for me. ;3", err)
return
}
// 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("DeviceStatus", "R")
break
case "GetChallenge":
fmt.Println("The request is valid! Responding...")
// 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.
// It then uses another hard-coded value in place of this returned value entirely in any situation.
// For this reason, we consider it irrelevant.
e.AddKVNode("Challenge", SharedChallenge)
break
case "GetRegistrationInfo":
reason := "how dirty. ;3"
accountId, err := getKey(doc, "AccountId")
if err != nil {
return e.ReturnError(7, reason, err)
}
deviceCode, err := getKey(doc, "DeviceCode")
if err != nil {
return e.ReturnError(7, reason, err)
}
fmt.Println("The request is valid! Responding...")
e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", "00000000")
e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", country)
e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("DeviceStatus", "R")
// This _must_ be POINTS.
e.AddKVNode("Currency", "POINTS")
break
case "Register":
reason := "disgustingly invalid. ;3"
deviceCode, err := getKey(doc, "DeviceCode")
if err != nil {
return e.ReturnError(7, reason, err)
}
registerRegion, err := getKey(doc, "RegisterRegion")
if err != nil {
return e.ReturnError(7, reason, err)
}
if registerRegion != region {
return e.ReturnError(7, reason, errors.New("region does not match registration region"))
}
serialNo, err := getKey(doc, "SerialNumber")
if err != nil {
return e.ReturnError(7, reason, err)
}
// Validate given friend code.
userId, err := strconv.ParseUint(deviceCode, 10, 64)
if err != nil {
return e.ReturnError(7, reason, err)
}
if wiino.NWC24CheckUserID(userId) != 0 {
return e.ReturnError(7, reason, err)
}
// Generate a random 9-digit number, padding zeros as necessary.
accountId := fmt.Sprintf("%9d", rand.Intn(999999999))
// This is where it gets hairy.
// Generate a device token, 21 characters...
deviceToken := RandString(21)
// ...and then its md5, because the Wii sends this...
md5DeviceToken := fmt.Sprintf("%x", md5.Sum([]byte(deviceToken)))
// ...and then the sha256 of that md5.
// 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.
// This is the hash of the md5 represented as a string, not individual byte values.
doublyHashedDeviceToken := fmt.Sprintf("%x", sha2562.Sum256([]byte(md5DeviceToken)))
// Insert all of our obtained values to the database..
_, err = registerUser.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, region, country, language, serialNo, deviceCode)
if err != nil {
// 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.Number == 1062 {
return e.ReturnError(7, reason, errors.New("user already exists"))
}
}
log.Printf("error executing statement: %v\n", err)
return e.ReturnError(7, reason, errors.New("failed to execute db operation"))
}
fmt.Println("The request is valid! Responding...")
e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", deviceToken)
e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", country)
// Optionally, one can send back DeviceCode and ExtAccountId to update on device.
// We send these back as-is regardless.
e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode)
break
case "Unregister":
// 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()
e.AddKVNode("OriginalSerialNumber", serialNo)
e.AddKVNode("DeviceStatus", "R")
}
func getChallenge(e *Envelope) {
// 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.
// It then uses another hard-coded value in place of this returned value entirely in any situation.
// For this reason, we consider it irrelevant.
e.AddKVNode("Challenge", SharedChallenge)
}
func getRegistrationInfo(e *Envelope) {
reason := "how dirty. ;3"
accountId, err := getKey(e.doc, "AccountId")
if err != nil {
e.Error(7, reason, err)
}
deviceCode, err := getKey(e.doc, "DeviceCode")
if err != nil {
e.Error(7, reason, err)
}
e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", "00000000")
e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", e.Country())
e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("DeviceStatus", "R")
// This _must_ be POINTS.
e.AddKVNode("Currency", "POINTS")
}
func register(e *Envelope) {
reason := "disgustingly invalid. ;3"
deviceCode, err := getKey(e.doc, "DeviceCode")
if err != nil {
e.Error(7, reason, err)
return
}
registerRegion, err := getKey(e.doc, "RegisterRegion")
if err != nil {
e.Error(7, reason, err)
return
}
if registerRegion != e.Region() {
e.Error(7, reason, errors.New("region does not match registration region"))
return
}
serialNo, err := getKey(e.doc, "SerialNumber")
if err != nil {
e.Error(7, reason, err)
return
}
// Validate given friend code.
userId, err := strconv.ParseUint(deviceCode, 10, 64)
if err != nil {
e.Error(7, reason, err)
return
}
if wiino.NWC24CheckUserID(userId) != 0 {
e.Error(7, reason, err)
return
}
// Generate a random 9-digit number, padding zeros as necessary.
accountId := fmt.Sprintf("%9d", rand.Intn(999999999))
// This is where it gets hairy.
// Generate a device token, 21 characters...
deviceToken := RandString(21)
// ...and then its md5, because the Wii sends this...
md5DeviceToken := fmt.Sprintf("%x", md5.Sum([]byte(deviceToken)))
// ...and then the sha256 of that md5.
// 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.
// This is the hash of the md5 represented as a string, not individual byte values.
doublyHashedDeviceToken := fmt.Sprintf("%x", sha256.Sum256([]byte(md5DeviceToken)))
// Insert all of our obtained values to the database..
_, err = registerUser.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, e.Region(), e.Country(), e.Language(), serialNo, deviceCode)
if err != nil {
// 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.Number == 1062 {
e.Error(7, reason, errors.New("user already exists"))
return
}
}
log.Printf("error executing statement: %v\n", err)
e.Error(7, reason, errors.New("failed to execute db operation"))
return
}
fmt.Println("The request is valid! Responding...")
e.AddKVNode("AccountId", accountId)
e.AddKVNode("DeviceToken", deviceToken)
e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", e.Country())
// Optionally, one can send back DeviceCode and ExtAccountId to update on device.
// We send these back as-is regardless.
e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceCode", deviceCode)
}
func unregister(e *Envelope) {
// how abnormal... ;3
}

94
main.go
View File

@ -25,7 +25,6 @@ import (
"io/ioutil"
"log"
"net/http"
"strings"
"time"
)
@ -76,79 +75,26 @@ func main() {
// 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)
// These following endpoints don't have to match what the official WSC have.
// However, semantically, it feels proper.
http.HandleFunc("/ecs/services/ECommerceSOAP", commonHandler)
http.HandleFunc("/ias/services/IdentityAuthenticationSOAP", commonHandler)
log.Fatal(http.ListenAndServe(CON.Address, nil))
r := NewRoute()
ecs := r.HandleGroup("ecs")
{
ecs.Authenticated("CheckDeviceStatus", checkDeviceStatus)
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.
}
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
import "encoding/xml"
import (
"encoding/xml"
"github.com/antchfx/xmlquery"
)
/////////////////////
// SOAP STRUCTURES //
@ -47,7 +50,12 @@ type Envelope struct {
Body Body
// 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,
@ -86,7 +94,7 @@ type KVField struct {
type Balance struct {
XMLName xml.Name `xml:"Balance"`
Amount int `xml:"Amount"`
Currency string `xml"Currency"`
Currency string `xml:"Currency"`
}
// Transactions represents a common XML structure.
@ -99,11 +107,11 @@ type Transactions struct {
// Tickets represents the format to inform a console of available titles for its consumption.
type Tickets struct {
XMLName xml.Name `xml:"Tickets"`
TicketId string `xml:"TicketId"`
TitleId string `xml:"TitleId"`
RevokeDate int `xml:"RevokeDate"`
Version int `xml:"Version"`
MigrateCount int `xml:"MigrateCount"`
MigrateLimit int `xml:"MigrateLimit"`
}
XMLName xml.Name `xml:"Tickets"`
TicketId string `xml:"TicketId"`
TitleId string `xml:"TitleId"`
RevokeDate int `xml:"RevokeDate"`
Version int `xml:"Version"`
MigrateCount int `xml:"MigrateCount"`
MigrateLimit int `xml:"MigrateLimit"`
}

View File

@ -25,6 +25,7 @@ import (
"io"
"math/rand"
"regexp"
"strings"
"time"
)
@ -46,11 +47,18 @@ func parseAction(original string) (string, string) {
}
// 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.
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/",
XSD: "http://www.w3.org/2001/XMLSchema",
XSI: "http://www.w3.org/2001/XMLSchema-instance",
@ -62,13 +70,16 @@ func NewEnvelope(service string, action string) Envelope {
TimeStamp: timestampNano,
},
},
action: action,
doc: doc,
}
}
// Action returns the action for this service.
func (e *Envelope) Action() string {
return e.action
// Obtain common request values.
err = e.ObtainCommon()
if err != nil {
return nil, err
}
return &e, nil
}
// Timestamp returns a shared timestamp for this request.
@ -81,9 +92,25 @@ func (e *Envelope) DeviceId() string {
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.
func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error {
func (e *Envelope) ObtainCommon() error {
var err error
doc := e.doc
// These fields are common across all requests.
e.Body.Response.Version, err = getKey(doc, "Version")
@ -99,6 +126,20 @@ func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error {
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
}
@ -117,7 +158,10 @@ func (e *Envelope) AddCustomType(customType interface{}) {
// 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) {
func (e *Envelope) becomeXML() (bool, string) {
// Non-zero error codes indicate a failure.
intendedStatus := e.Body.Response.ErrorCode == 0
contents, err := xml.Marshal(e)
if err != nil {
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.
func (e *Envelope) ReturnSuccess() (bool, string) {
// 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) {
// 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.
@ -145,8 +181,6 @@ func (e *Envelope) ReturnError(errorCode int, reason string, err error) (bool, s
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.