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 (
|
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
260
ias.go
@ -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
94
main.go
@ -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
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
|
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"`
|
||||||
}
|
}
|
||||||
|
76
utils.go
76
utils.go
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user