mirror of
https://wiilab.wiimart.org/wiimart/WiiSOAP
synced 2025-09-05 21:11:02 +02:00
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:
parent
e9445110b0
commit
37fb20d8df
160
ecs.go
160
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")
|
||||
}
|
||||
|
260
ias.go
260
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
|
||||
}
|
||||
|
94
main.go
94
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)
|
||||
}
|
||||
|
141
route.go
Normal file
141
route.go
Normal 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)
|
||||
}
|
30
structure.go
30
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"`
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
76
utils.go
76
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user