From 37fb20d8df0e1b5df5a556c68a32875945c9cfa5 Mon Sep 17 00:00:00 2001 From: Spotlight Date: Sat, 5 Sep 2020 08:58:01 -0500 Subject: [PATCH] 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. --- ecs.go | 160 ++++++++++++++----------------- ias.go | 260 +++++++++++++++++++++++---------------------------- main.go | 94 ++++--------------- route.go | 141 ++++++++++++++++++++++++++++ structure.go | 30 +++--- utils.go | 76 ++++++++++----- 6 files changed, 425 insertions(+), 336 deletions(-) create mode 100644 route.go diff --git a/ecs.go b/ecs.go index d3555d4..ee3f7ad 100644 --- a/ecs.go +++ b/ecs.go @@ -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") } diff --git a/ias.go b/ias.go index 3358a61..fd82d4b 100644 --- a/ias.go +++ b/ias.go @@ -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 } diff --git a/main.go b/main.go index f649423..7e988d6 100644 --- a/main.go +++ b/main.go @@ -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) -} diff --git a/route.go b/route.go new file mode 100644 index 0000000..ff2ef91 --- /dev/null +++ b/route.go @@ -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) +} diff --git a/structure.go b/structure.go index e44d587..b0b2484 100644 --- a/structure.go +++ b/structure.go @@ -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"` -} \ No newline at end of file + 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"` +} diff --git a/utils.go b/utils.go index 83f3f73..62d2dc8 100644 --- a/utils.go +++ b/utils.go @@ -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.