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 ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/antchfx/xmlquery"
"log" "log"
) )
@ -36,96 +35,79 @@ func ecsInitialize() {
} }
} }
func ecsHandler(e Envelope, doc *xmlquery.Node) (bool, string) { func checkDeviceStatus(e *Envelope) {
// All actions below are for ECS-related functions. // You need to POST some SOAP from WSC if you wanna get some, honey. ;3
switch e.Action() { e.AddCustomType(Balance{
// TODO: Make the case functions cleaner. (e.g. Should the response be a variable?) Amount: 2018,
// TODO: Update the responses so that they query the SQL Database for the proper information (e.g. Device Code, Token, etc). Currency: "POINTS",
})
e.AddKVNode("ForceSyncTime", "0")
e.AddKVNode("ExtTicketTime", e.Timestamp())
e.AddKVNode("SyncTime", e.Timestamp())
}
case "CheckDeviceStatus": func notifyETicketsSynced(e *Envelope) {
//You need to POST some SOAP from WSC if you wanna get some, honey. ;3 // This is a disgusting request, but 20 dollars is 20 dollars. ;3
}
fmt.Println("The request is valid! Responding...") func listETickets(e *Envelope) {
e.AddCustomType(Balance{ fmt.Println("The request is valid! Responding...")
Amount: 2018, rows, err := ownedTitles.Query("todo, sorry")
Currency: "POINTS", if err != nil {
}) e.Error(2, "that's all you've got for me? ;3", err)
e.AddKVNode("ForceSyncTime", "0") return
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."
} }
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 ( 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,145 +40,124 @@ 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. e.AddKVNode("OriginalSerialNumber", serialNo)
switch e.Action() { e.AddKVNode("DeviceStatus", "R")
// 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 getChallenge(e *Envelope) {
case "CheckRegistration": // The official Wii Shop Channel requests a Challenge from the server, and promptly disregards it.
serialNo, err := getKey(doc, "SerialNumber") // (Sometimes, it may not request a challenge at all.) No attempt is made to validate the response.
if err != nil { // It then uses another hard-coded value in place of this returned value entirely in any situation.
return e.ReturnError(5, "not good enough for me. ;3", err) // For this reason, we consider it irrelevant.
} e.AddKVNode("Challenge", SharedChallenge)
fmt.Println("The request is valid! Responding...") }
e.AddKVNode("OriginalSerialNumber", serialNo)
e.AddKVNode("DeviceStatus", "R") func getRegistrationInfo(e *Envelope) {
break reason := "how dirty. ;3"
accountId, err := getKey(e.doc, "AccountId")
case "GetChallenge": if err != nil {
fmt.Println("The request is valid! Responding...") e.Error(7, reason, err)
// 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. deviceCode, err := getKey(e.doc, "DeviceCode")
// For this reason, we consider it irrelevant. if err != nil {
e.AddKVNode("Challenge", SharedChallenge) e.Error(7, reason, err)
break }
case "GetRegistrationInfo": e.AddKVNode("AccountId", accountId)
reason := "how dirty. ;3" e.AddKVNode("DeviceToken", "00000000")
accountId, err := getKey(doc, "AccountId") e.AddKVNode("DeviceTokenExpired", "false")
if err != nil { e.AddKVNode("Country", e.Country())
return e.ReturnError(7, reason, err) e.AddKVNode("ExtAccountId", "")
} e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("DeviceStatus", "R")
deviceCode, err := getKey(doc, "DeviceCode") // This _must_ be POINTS.
if err != nil { e.AddKVNode("Currency", "POINTS")
return e.ReturnError(7, reason, err) }
}
func register(e *Envelope) {
fmt.Println("The request is valid! Responding...") reason := "disgustingly invalid. ;3"
e.AddKVNode("AccountId", accountId) deviceCode, err := getKey(e.doc, "DeviceCode")
e.AddKVNode("DeviceToken", "00000000") if err != nil {
e.AddKVNode("DeviceTokenExpired", "false") e.Error(7, reason, err)
e.AddKVNode("Country", country) return
e.AddKVNode("ExtAccountId", "") }
e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("DeviceStatus", "R") registerRegion, err := getKey(e.doc, "RegisterRegion")
// This _must_ be POINTS. if err != nil {
e.AddKVNode("Currency", "POINTS") e.Error(7, reason, err)
break return
}
case "Register": if registerRegion != e.Region() {
reason := "disgustingly invalid. ;3" e.Error(7, reason, errors.New("region does not match registration region"))
deviceCode, err := getKey(doc, "DeviceCode") return
if err != nil { }
return e.ReturnError(7, reason, err)
} serialNo, err := getKey(e.doc, "SerialNumber")
if err != nil {
registerRegion, err := getKey(doc, "RegisterRegion") e.Error(7, reason, err)
if err != nil { return
return e.ReturnError(7, reason, err) }
}
if registerRegion != region { // Validate given friend code.
return e.ReturnError(7, reason, errors.New("region does not match registration region")) userId, err := strconv.ParseUint(deviceCode, 10, 64)
} if err != nil {
e.Error(7, reason, err)
serialNo, err := getKey(doc, "SerialNumber") return
if err != nil { }
return e.ReturnError(7, reason, err) if wiino.NWC24CheckUserID(userId) != 0 {
} e.Error(7, reason, err)
return
// Validate given friend code. }
userId, err := strconv.ParseUint(deviceCode, 10, 64)
if err != nil { // Generate a random 9-digit number, padding zeros as necessary.
return e.ReturnError(7, reason, err) accountId := fmt.Sprintf("%9d", rand.Intn(999999999))
}
if wiino.NWC24CheckUserID(userId) != 0 { // This is where it gets hairy.
return e.ReturnError(7, reason, err) // Generate a device token, 21 characters...
} deviceToken := RandString(21)
// ...and then its md5, because the Wii sends this...
// Generate a random 9-digit number, padding zeros as necessary. md5DeviceToken := fmt.Sprintf("%x", md5.Sum([]byte(deviceToken)))
accountId := fmt.Sprintf("%9d", rand.Intn(999999999)) // ...and then the sha256 of that md5.
// We'll store this in our database, as storing the md5 itself is effectively the token.
// This is where it gets hairy. // It would not be good for security to directly store the token either.
// Generate a device token, 21 characters... // This is the hash of the md5 represented as a string, not individual byte values.
deviceToken := RandString(21) doublyHashedDeviceToken := fmt.Sprintf("%x", sha256.Sum256([]byte(md5DeviceToken)))
// ...and then its md5, because the Wii sends this...
md5DeviceToken := fmt.Sprintf("%x", md5.Sum([]byte(deviceToken))) // Insert all of our obtained values to the database..
// ...and then the sha256 of that md5. _, err = registerUser.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, e.Region(), e.Country(), e.Language(), serialNo, deviceCode)
// We'll store this in our database, as storing the md5 itself is effectively the token. if err != nil {
// It would not be good for security to directly store the token either. // It's okay if this isn't a MySQL error, as perhaps other issues have come in.
// This is the hash of the md5 represented as a string, not individual byte values. if driverErr, ok := err.(*mysql.MySQLError); ok {
doublyHashedDeviceToken := fmt.Sprintf("%x", sha2562.Sum256([]byte(md5DeviceToken))) if driverErr.Number == 1062 {
e.Error(7, reason, errors.New("user already exists"))
// Insert all of our obtained values to the database.. return
_, 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. log.Printf("error executing statement: %v\n", err)
if driverErr, ok := err.(*mysql.MySQLError); ok { e.Error(7, reason, errors.New("failed to execute db operation"))
if driverErr.Number == 1062 { return
return e.ReturnError(7, reason, errors.New("user already exists")) }
}
} fmt.Println("The request is valid! Responding...")
log.Printf("error executing statement: %v\n", err) e.AddKVNode("AccountId", accountId)
return e.ReturnError(7, reason, errors.New("failed to execute db operation")) e.AddKVNode("DeviceToken", deviceToken)
} e.AddKVNode("DeviceTokenExpired", "false")
e.AddKVNode("Country", e.Country())
fmt.Println("The request is valid! Responding...") // Optionally, one can send back DeviceCode and ExtAccountId to update on device.
e.AddKVNode("AccountId", accountId) // We send these back as-is regardless.
e.AddKVNode("DeviceToken", deviceToken) e.AddKVNode("ExtAccountId", "")
e.AddKVNode("DeviceTokenExpired", "false") e.AddKVNode("DeviceCode", deviceCode)
e.AddKVNode("Country", country) }
// Optionally, one can send back DeviceCode and ExtAccountId to update on device.
// We send these back as-is regardless. func unregister(e *Envelope) {
e.AddKVNode("ExtAccountId", "") // how abnormal... ;3
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()
} }

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.
@ -99,11 +107,11 @@ type Transactions struct {
// Tickets represents the format to inform a console of available titles for its consumption. // Tickets represents the format to inform a console of available titles for its consumption.
type Tickets struct { type Tickets struct {
XMLName xml.Name `xml:"Tickets"` XMLName xml.Name `xml:"Tickets"`
TicketId string `xml:"TicketId"` TicketId string `xml:"TicketId"`
TitleId string `xml:"TitleId"` TitleId string `xml:"TitleId"`
RevokeDate int `xml:"RevokeDate"` RevokeDate int `xml:"RevokeDate"`
Version int `xml:"Version"` Version int `xml:"Version"`
MigrateCount int `xml:"MigrateCount"` MigrateCount int `xml:"MigrateCount"`
MigrateLimit int `xml:"MigrateLimit"` MigrateLimit int `xml:"MigrateLimit"`
} }

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.