diff --git a/nas/acctcreate.go b/nas/acctcreate.go deleted file mode 100644 index 4d29e52..0000000 --- a/nas/acctcreate.go +++ /dev/null @@ -1,14 +0,0 @@ -package nas - -import ( - "strconv" - "wwfc/database" -) - -func acctcreate(r *Response, fields map[string]string) map[string]string { - return map[string]string{ - "retry": "0", - "returncd": "002", - "userid": strconv.FormatInt(database.GetUniqueUserID(), 10), - } -} diff --git a/nas/auth.go b/nas/auth.go new file mode 100644 index 0000000..58dad6b --- /dev/null +++ b/nas/auth.go @@ -0,0 +1,110 @@ +package nas + +import ( + "github.com/logrusorgru/aurora/v3" + "net/http" + "net/url" + "strconv" + "strings" + "wwfc/common" + "wwfc/database" + "wwfc/logging" +) + +func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + logging.Error(moduleName, "Failed to parse form") + return + } + + fields := map[string]string{} + for key, values := range r.PostForm { + if len(values) != 1 { + logging.Warn(moduleName, "Ignoring multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values)) + continue + } + + parsed, err := common.Base64DwcEncoding.DecodeString(values[0]) + if err != nil { + logging.Error(moduleName, "Invalid POST form value:", aurora.Cyan(key).String()+":", aurora.Cyan(values[0])) + return + } + logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(string(parsed))) + fields[key] = string(parsed) + } + + action, ok := fields["action"] + if !ok || action == "" { + logging.Error(moduleName, "No action in form") + return + } + + reply := map[string]string{} + + switch action { + case "login": + reply = login(moduleName, fields) + break + + case "acctcreate": + reply = acctcreate(moduleName, fields) + break + } + + param := url.Values{} + for key, value := range reply { + param.Set(key, common.Base64DwcEncoding.EncodeToString([]byte(value))) + } + response := []byte(param.Encode()) + response = []byte(strings.Replace(string(response), "%2A", "*", -1)) + // DWC treats the response like a null terminated string + response = append(response, 0x00) + + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Content-Length", strconv.Itoa(len(response))) + w.Write(response) +} + +func acctcreate(moduleName string, fields map[string]string) map[string]string { + return map[string]string{ + "retry": "0", + "returncd": "002", + "userid": strconv.FormatInt(database.GetUniqueUserID(), 10), + } +} + +func login(moduleName string, fields map[string]string) map[string]string { + param := map[string]string{ + "retry": "0", + "locator": "gs.wiilink24.com", + } + + strUserId, ok := fields["userid"] + if !ok { + logging.Error(moduleName, "No userid in form") + param["returncd"] = "103" + return param + } + + userId, err := strconv.ParseInt(strUserId, 10, 64) + if err != nil { + logging.Error(moduleName, "Invalid userid string in form") + param["returncd"] = "103" + return param + } + + gsbrcd, ok := fields["gsbrcd"] + if !ok { + logging.Error(moduleName, "No gsbrcd in form") + param["returncd"] = "103" + return param + } + + authToken, challenge := database.GenerateAuthToken(pool, ctx, userId, gsbrcd) + + param["returncd"] = "001" + param["challenge"] = challenge + param["token"] = authToken + return param +} diff --git a/nas/login.go b/nas/login.go deleted file mode 100644 index 3de5ae3..0000000 --- a/nas/login.go +++ /dev/null @@ -1,44 +0,0 @@ -package nas - -import ( - "strconv" - "wwfc/database" - "wwfc/logging" -) - -func login(r *Response, fields map[string]string) map[string]string { - moduleName := "NAS:" + r.request.RemoteAddr - - param := map[string]string{ - "retry": "0", - "locator": "gamespy.com", - } - - strUserId, ok := fields["userid"] - if !ok { - logging.Error(moduleName, "No userid in form") - param["returncd"] = "103" - return param - } - - userId, err := strconv.ParseInt(strUserId, 10, 64) - if err != nil { - logging.Error(moduleName, "Invalid userid string in form") - param["returncd"] = "103" - return param - } - - gsbrcd, ok := fields["gsbrcd"] - if !ok { - logging.Error(moduleName, "No gsbrcd in form") - param["returncd"] = "103" - return param - } - - authToken, challenge := database.GenerateAuthToken(pool, ctx, userId, gsbrcd) - - param["returncd"] = "001" - param["challenge"] = challenge - param["token"] = authToken - return param -} diff --git a/nas/main.go b/nas/main.go index 3d9fc15..6e5871c 100644 --- a/nas/main.go +++ b/nas/main.go @@ -4,10 +4,16 @@ import ( "context" "fmt" "github.com/jackc/pgx/v4/pgxpool" + "github.com/logrusorgru/aurora/v3" "log" + "net/http" + "regexp" + "strconv" + "strings" "wwfc/common" "wwfc/logging" "wwfc/nhttp" + "wwfc/sake" ) var ( @@ -32,20 +38,52 @@ func StartServer() { } address := config.Address + ":" + config.Port - r := NewRoute() - ac := r.HandleGroup("ac") - { - ac.HandleAction("acctcreate", acctcreate) - ac.HandleAction("login", login) - } - - // TODO: Hack lol - p0 := r.HandleGroup("p0") - { - p0.HandleAction("acctcreate", getStage1) - p0.HandleAction("login", getStage1) - } logging.Notice("NAS", "Starting HTTP server on", address) - log.Fatal(nhttp.ListenAndServe(address, r.Handle())) + log.Fatal(nhttp.ListenAndServe(address, http.HandlerFunc(handleRequest))) +} + +var regexSakeHost = regexp.MustCompile(`^([a-z\-]+\.)?sake\.gs\.`) +var regexStage1URL = regexp.MustCompile(`^/p([0-9])$`) + +func handleRequest(w http.ResponseWriter, r *http.Request) { + // TODO: Move this to its own server + // Check for *.sake.gs.* or sake.gs.* + if regexSakeHost.MatchString(r.Host) { + // Redirect to the sake server + sake.HandleRequest(w, r) + return + } + + logging.Notice("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr)) + moduleName := "NAS:" + r.RemoteAddr + + if r.URL.String() == "/ac" { + handleAuthRequest(moduleName, w, r) + return + } + + // TODO: Move this to its own server + // Check for /payload + if strings.HasPrefix(r.URL.String(), "/payload?") { + handlePayloadRequest(moduleName, w, r) + return + } + + // Check for /online + if r.URL.String() == "/online" { + returnOnlineStats(w) + return + } + + // Stage 1 + if match := regexStage1URL.FindStringSubmatch(r.URL.String()); match != nil { + val, err := strconv.Atoi(match[1]) + if err != nil { + panic(err) + } + + downloadStage1(moduleName, w, r, val) + return + } } diff --git a/nas/payload.go b/nas/payload.go index bc1939b..3694761 100644 --- a/nas/payload.go +++ b/nas/payload.go @@ -17,14 +17,17 @@ import ( "wwfc/logging" ) -func getStage1(r *Response, fields map[string]string) map[string]string { +func downloadStage1(moduleName string, w http.ResponseWriter, r *http.Request, stage1Ver int) { dat, err := os.ReadFile("payload/stage1.bin") if err != nil { panic(err) } - r.payload = append([]byte{0x01, 0x2C}, dat...) - return map[string]string{} + payload := append([]byte{0x01, 0x2C}, dat...) + + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Content-Length", strconv.Itoa(len(payload))) + w.Write(payload) } func handlePayloadRequest(moduleName string, w http.ResponseWriter, r *http.Request) { diff --git a/nas/profanity.go b/nas/profanity.go new file mode 100644 index 0000000..7630f6d --- /dev/null +++ b/nas/profanity.go @@ -0,0 +1 @@ +package nas diff --git a/nas/route.go b/nas/route.go deleted file mode 100644 index b693278..0000000 --- a/nas/route.go +++ /dev/null @@ -1,156 +0,0 @@ -package nas - -import ( - "encoding/base64" - "github.com/logrusorgru/aurora/v3" - "net/http" - "net/url" - "regexp" - "strconv" - "strings" - "wwfc/logging" - "wwfc/sake" -) - -type Route struct { - Actions []Action -} - -// Action contains information about how a specified action should be handled. -type Action struct { - ActionName string - Callback func(*Response, map[string]string) map[string]string - ServiceType string -} - -func NewRoute() Route { - return Route{} -} - -// 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 (route *Route) HandleGroup(serviceType string) RoutingGroup { - return RoutingGroup{ - Route: route, - ServiceType: serviceType, - } -} - -func (r *RoutingGroup) HandleAction(action string, function func(*Response, map[string]string) map[string]string) { - r.Route.Actions = append(r.Route.Actions, Action{ - ActionName: action, - Callback: function, - ServiceType: r.ServiceType, - }) -} - -var ( - regexSakeURL = regexp.MustCompile(`^([a-z\-]+\.)?sake\.gs\.`) -) - -func (route *Route) Handle() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO: Move this to its own server - // Check for *.sake.gs.* or sake.gs.* - if regexSakeURL.MatchString(r.Host) { - // Redirect to the sake server - sake.HandleRequest(w, r) - return - } - - logging.Notice("NAS", aurora.Yellow(r.Method), aurora.Cyan(r.URL), "via", aurora.Cyan(r.Host), "from", aurora.BrightCyan(r.RemoteAddr)) - moduleName := "NAS:" + r.RemoteAddr - - // TODO: Move this to its own server - // Check for /payload - if strings.HasPrefix(r.URL.String(), "/payload") { - handlePayloadRequest(moduleName, w, r) - return - } - - // Check for /online - if strings.HasPrefix(r.URL.String(), "/online") { - returnOnlineStats(w) - return - } - - err := r.ParseForm() - if err != nil { - logging.Error(moduleName, "Failed to parse form") - return - } - - if !strings.HasPrefix(r.URL.Path, "/") { - logging.Error(moduleName, "Invalid URL") - return - } - - path := r.URL.Path[1:] - - fields := map[string]string{} - for key, values := range r.PostForm { - if len(values) != 1 { - logging.Warn(moduleName, "Ignoring multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values)) - continue - } - - parsed, err := base64.StdEncoding.DecodeString(strings.Replace(values[0], "*", "=", -1)) - if err != nil { - logging.Error(moduleName, "Invalid POST form value:", aurora.Cyan(key).String()+":", aurora.Cyan(values[0])) - return - } - logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(string(parsed))) - fields[key] = string(parsed) - } - - actionName, ok := fields["action"] - if !ok || actionName == "" { - logging.Error(moduleName, "No action in form") - return - } - - var action Action - for _, _action := range route.Actions { - if path == _action.ServiceType && actionName == _action.ActionName { - action = _action - } - } - - // Make sure we found an action - if action.ActionName == "" && action.ServiceType == "" { - logging.Error(moduleName, "No action for", aurora.Cyan(actionName)) - return - } - - response := NewResponse(&w, r) - reply := action.Callback(response, fields) - - if len(reply) != 0 { - param := url.Values{} - for key, value := range reply { - param.Set(key, strings.Replace(base64.StdEncoding.EncodeToString([]byte(value)), "=", "*", -1)) - } - response.payload = []byte(param.Encode()) - response.payload = []byte(strings.Replace(string(response.payload), "%2A", "*", -1)) - // DWC treats the response like a null terminated string - response.payload = append(response.payload, 0x00) - } - - w.Header().Set("NODE", "wifiappe1") - w.Header().Set("Content-Type", "text/plain") - w.Header().Set("Content-Length", strconv.Itoa(len(response.payload))) - w.Write(response.payload) - }) -} - -func NewResponse(w *http.ResponseWriter, r *http.Request) *Response { - return &Response{ - request: r, - writer: w, - } -} diff --git a/nas/structure.go b/nas/structure.go deleted file mode 100644 index 8d20332..0000000 --- a/nas/structure.go +++ /dev/null @@ -1,9 +0,0 @@ -package nas - -import "net/http" - -type Response struct { - request *http.Request - writer *http.ResponseWriter - payload []byte -}