-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcmoc_fetch.go
166 lines (142 loc) · 5.46 KB
/
cmoc_fetch.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package main
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
)
// As of 2024-10-05, this is the WiiLink Check Mii Out Channel API
// would be able to request this in JS if it returned CORS headers :(
const (
cmocSearchEndpoint = "https://miicontestp.wii.rc24.xyz/cgi-bin/search.cgi?entryno=%s"
cmocFileSuffix = ".rsd" // content-disposition file extension
)
// Parses a CMOC entry number string to a string usable
// as entryno on the API(s), adapted from mii2studio.py
func cmocEntryStringParse(input string) (string, error) {
// Strip dashes, convert to an integer, then to a binary string
numStr := strings.ReplaceAll(input, "-", "")
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return "", err
}
// Pad to 40 characters, and take slice from 8th bit onward
binaryStr := fmt.Sprintf("%032b", num)
paddedBinaryStr := fmt.Sprintf("%040s", binaryStr)
trimmedBinary := paddedBinaryStr[8:]
num, _ = strconv.ParseInt(trimmedBinary, 2, 64)
// we created that binary string so shouldn't parse wrong
// Scramble the number using bitwise operations
// NOTE: did not look into which binary this came from
num ^= 0x20070419
num ^= (num >> 0x1D) ^ (num >> 0x11) ^ (num >> 0x17)
num ^= (num & 0xF0F0F0F) << 4
num ^= ((num << 0x1E) ^ (num << 0x12) ^ (num << 0x18)) & 0xFFFFFFFF
// Return the final descrambled number as a string
return strconv.FormatInt(num, 10), nil
}
// Predefined errors for response statuses
var (
errCMOCEndpointReturnedNon200 = errors.New("mii probably not found (CMOC endpoint returned non-200 status)")
errCMOCResponseTooShort = errors.New("response from CMOC endpoint is too short")
errCMOCResponseNotFound = errors.New("mii not found")
)
// Requests a CMOC entry from the endpoint defined in cmocSearchEndpoint
// and then cuts out the output (RFLStoreData) from the response
func lookupCMOCAndCutoutMiiData(entryNo string) ([]byte, error) {
url := fmt.Sprintf(cmocSearchEndpoint, entryNo)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// NOTE: does not account for 10x, 30x but whatever
if resp.StatusCode != 200 {
return nil, errCMOCEndpointReturnedNon200
}
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// according to mii2studio.py...
if len(body) == 32 { // 32 = empty response
return nil, errCMOCResponseNotFound
}
// Extract the RFLStoreData portion
if len(body) < 188 {
return nil, errCMOCResponseTooShort
}
binaryData := body[56:132]
return binaryData, nil
}
//var cmocCodeRegex = regexp.MustCompile(`^\d{4}-\d{4}-\d{4}$`)
const cmocLookupHandlerUsageSuffix = `
this code should be just, the CMOC entry number
in the form: 1234-5678-9123 - we will descramble it
the search endpoint is: ` + cmocSearchEndpoint + `
soooo as of writing this, the site is accessed at: https://miicontest.wiilink.ca`
// looks up a check mii out code to RFLStoreData
// cmocLookupHandler looks up a check mii out code to RFLStoreData
// @Summary Lookup CMOC Entry
// @Description Looks up a CMOC entry number from the Check Mii Out Channel and returns its Mii data as 76 byte RFLStoreData/.rsd format. As of writing this code, it's retrieving from WiiLink: https://miicontest.wiilink.ca
// @Tags Fetch Mii Data
// @Accept json,octet-stream
// @Produce json,octet-stream
// @Param cmoc_code path string true "CMOC Entry Number (we will descramble it)" Format(string, 1234-5678-9123)
// @Success 200 {object} string "Base64 encoded RFLStoreData (76 bytes) or binary data if requesting with Accept: application/octet-stream"
// @Failure 400 {object} string "Bad Request"
// @Failure 404 {object} string "Not Found"
// @Failure 500 {object} string "Internal Server Error"
// @Router /cmoc_lookup/{cmoc_code} [get]
func cmocLookupHandler(w http.ResponseWriter, r *http.Request) {
setCORSHeaders(w, r) // nnid_fetch.go
// Split the URL path and validate input format
parts := strings.Split(r.URL.Path, "/")
if len(parts) != 3 || parts[2] == "" {
http.Error(w, "usage: " + cmocLookupHandlerPrefix + "(cmoc code)" + cmocLookupHandlerUsageSuffix, http.StatusBadRequest)
return
}
/*
if !cmocCodeRegex.MatchString(parts[2]) {
http.Error(w, "Invalid code format. Expected '1234-5678-9123'", http.StatusBadRequest)
return
}
*/
inputCode := parts[2]
// Descramble the input code to get the CMOC entryNo
entryNo, err := cmocEntryStringParse(inputCode)
if err != nil {
http.Error(w, "failed to parse entry as number", http.StatusInternalServerError)
return
}
// Lookup the CMOC entry and return RFLStoreData
data, err := lookupCMOCAndCutoutMiiData(entryNo)
if err != nil {
if err == errCMOCResponseNotFound {
// return 404 for this specific error
http.Error(w, err.Error(), http.StatusNotFound)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Check Accept header to determine response format
acceptsOctetStream := r.Header.Get("Accept") == "application/octet-stream"
if acceptsOctetStream {
// Return as octet-stream with Content-Disposition header
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s%s\"", inputCode, cmocFileSuffix))
w.WriteHeader(http.StatusOK)
w.Write(data)
} else {
// Otherwise return as base64 encoded string
encoded := base64.StdEncoding.EncodeToString(data)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(encoded))
}
}