diff --git a/Makefile b/Makefile index 77522f7..4cccaab 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ SOURCES := source INCLUDES := source # Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer -VERSION := 3.1.5 +VERSION := 3.1.6 APP_TITLE := MBMNET APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk diff --git a/source/admin.c b/source/admin.c index 07b567b..273ef5d 100644 --- a/source/admin.c +++ b/source/admin.c @@ -1,138 +1,127 @@ -// Entwickler-Werkzeuge nur für Admins - und nur, wenn der Server es gerade bestätigt hat: -// 1. Die App liest aus dem Konto-Dienst (act) die Tokens des angemeldeten MBMNET-Kontos. Die hat nur ein 3DS, -// auf dem man sich wirklich mit diesem Konto angemeldet hat (ausgestellt vom MBMNET-Account-Server). -// 2. Sie schickt sie per HTTPS (MBMNET-CA) an /mbmnet/admin-check. Der Server prüft sie gegen seine -// Datenbank und antwortet admin=1 nur für Admins. -// Nichts wird gemerkt: Ohne Antwort vom Server (offline, Nintendo aktiv, kein MBMNET-Konto) keine Werkzeuge. +// Entwickler-Werkzeuge nur für Admins - und nur, wenn der Server es gerade bestätigt hat. +// +// Die Konsole hat einen geheimen Geräte-Schlüssel (32 Zufallsbytes, /3ds/mbmnet/geraete-schluessel). Die App +// schickt ihn per HTTPS (MBMNET-CA) an /mbmnet/admin-check: +// - freigeschaltete Konsole eines Admins -> admin=1 +// - unbekannt -> pair=123456: Diesen Code gibt ein Admin auf der Webseite unter Admin > Konsolen ein. +// Kein Passwort am 3DS, kein Konto-Dienst nötig. Ohne Antwort vom Server gibt es keine Werkzeuge. +// (Früher über die NNID-Anmeldung im Konto-Dienst - die war im MBMNET-Platz durcheinander und ließ sich +// nicht zuverlässig nutzen.) #include <3ds.h> #include #include +#include #include "admin.h" #include "https.h" +#define KEY_FILE "sdmc:/3ds/mbmnet/geraete-schluessel" #define OLD_CACHE_FILE "sdmc:/3ds/mbmnet/admin" // gemerkte Antwort älterer Versionen (wird gelöscht) -static volatile bool isAdmin; +static volatile bool isAdmin, running; +static Thread thread; static char server[64]; -static char why[160] = "Werkzeuge: kein MBMNET-Konto angemeldet."; // warum (noch) keine Werkzeuge -static Result lastAct; // letzter Fehler von act (für die Wiederholung) -static bool wanted; // MBMNET-Konto angemeldet: prüfen -static u8 mbmnetSlot; // Konto-Platz des MBMNET-Kontos (act) +static char why[160] = "Werkzeuge: Server prüft noch ..."; +static char key[65]; -// Liest die Tokens des angemeldeten Kontos aus dem Konto-Dienst (lokal, ohne Server-Anfrage von act) und lässt -// sie vom MBMNET-Server prüfen. Die Tokens hat dieser Server beim Anmelden ausgestellt, er kann sie selbst prüfen. -// (Früher: Service-Token über act angefordert - act lehnte das mit "ungültiger Zustand" ab, C8A14F4B.) -static Result read_token(u8 slot, AccountAccessToken *at) +// Geräte-Schlüssel laden oder einmal erzeugen (Zufall vom SSL-Dienst des 3DS) +static bool load_key(void) { - memset(at, 0, sizeof(*at)); - return ACT_GetAccountInfo(at, sizeof(*at), slot, INFO_TYPE_ACCOUNT_ACCESS_TOKEN); + FILE *f = fopen(KEY_FILE, "r"); + if (f) { + if (fgets(key, sizeof(key), f)) key[strcspn(key, "\r\n")] = 0; + fclose(f); + if (strlen(key) == 64) return true; + } + u8 raw[32]; + bool ok = false; + if (R_SUCCEEDED(sslcInit(0))) { + ok = R_SUCCEEDED(sslcGenerateRandomData(raw, sizeof(raw))); + sslcExit(); + } + if (!ok) { key[0] = 0; return false; } + for (int i = 0; i < 32; i++) snprintf(key + i * 2, 3, "%02x", raw[i]); + mkdir("sdmc:/3ds/mbmnet", 0777); + f = fopen(KEY_FILE, "w"); + if (!f) { key[0] = 0; return false; } + fputs(key, f); + fclose(f); + return true; } -// Leeres Passwort-Feld: libctru kopiert das Passwort immer - NULL ließ die App abstürzen (Luma-Dump, far=0) -static AccountPassword noPassword; - -// Konto bei act einloggen (wie Spiele vor dem Online-Gehen): mit dem gespeicherten Passwort, Ergebnis per Ereignis -static Result login(u8 slot) +static void check(void *arg) { - Handle done; - Result rc = svcCreateEvent(&done, RESET_ONESHOT); - if (R_FAILED(rc)) return rc; - // Erst mit dem gespeicherten Passwort (useNullPassword = false, leeres Feld), dann die andere Variante. - // Mit true schickte act ein leeres Passwort (Server: "falsches Passwort"). - for (int variant = 0; variant < 2; variant++) { - rc = ACTA_AcquireAccountTokenEx(slot, &noPassword, variant == 1, done); - if (R_SUCCEEDED(rc)) rc = svcWaitSynchronization(done, 15ULL * 1000 * 1000 * 1000); - if (rc == 0) rc = ACT_GetLastResponseResult(); - if (R_SUCCEEDED(rc)) break; - } - svcCloseHandle(done); - return rc; -} - -static void check_once(void) -{ - lastAct = 0; - u8 current = 0, slot = mbmnetSlot; - ACT_GetCommonInfo(¤t, sizeof(current), INFO_TYPE_COMMON_CURRENT_ACCOUNT_SLOT); - if (!slot) slot = current; - // act steht auf einem anderen Platz (z. B. Pretendo nach einem Soft-Reboot): auf den MBMNET-Platz stellen - Result rs = 0; - if (slot && current != slot) { - snprintf(why, sizeof(why), "Werkzeuge: stelle Konto-Dienst auf Platz %u (war %u) ...", slot, current); - ACTA_UnloadConsoleAccount(); - rs = ACTA_SetDefaultAccount(slot); - if (R_SUCCEEDED(rs)) ACT_Save(); - } - AccountId idHere = { 0 }, idCur = { 0 }; - ACT_GetAccountInfo(idHere, sizeof(idHere), slot, INFO_TYPE_ACCOUNT_ID); - if (current && current != slot) ACT_GetAccountInfo(idCur, sizeof(idCur), current, INFO_TYPE_ACCOUNT_ID); - static AccountAccessToken at; - // 1. direkt lesen; 2. sonst Konto laden (falls nach dem Umschalten nicht geladen); 3. sonst einloggen - // Fortschritt in "why": SELECT zeigt, bei welchem Schritt es gerade ist (oder hängt) - snprintf(why, sizeof(why), "Werkzeuge: Schritt 1/4 - Anmeldung lesen (Platz %u) ...", slot); - Result r1 = read_token(slot, &at), r2 = 0, r3 = 0, r4 = 0; - if (R_FAILED(r1) || !at.refreshToken[0]) { - snprintf(why, sizeof(why), "Werkzeuge: Schritt 2/4 - Konto laden ...\n(Lesen: %08lX)", (unsigned long)r1); - r2 = ACTA_LoadConsoleAccount(slot, false, &noPassword, false, false); - snprintf(why, sizeof(why), "Werkzeuge: Schritt 3/4 - Konto einloggen ...\n(Lesen %08lX, Laden %08lX)", (unsigned long)r1, (unsigned long)r2); - r3 = login(slot); - r4 = read_token(slot, &at); - } - if (!at.accessToken[0] && !at.refreshToken[0]) { - lastAct = R_FAILED(r4) ? r4 : r1; - snprintf(why, sizeof(why), "Werkzeuge: Konto \"%s\" (Platz %u%s%s) nicht eingeloggt.\nL %08lX A %08lX T %08lX/%08lX", - idHere, slot, idCur[0] ? ", act war auf " : "", idCur, (unsigned long)r2, (unsigned long)r3, (unsigned long)r1, (unsigned long)r4); + (void)arg; + if (!key[0] && !load_key()) { + snprintf(why, sizeof(why), "Werkzeuge: Geräte-Schlüssel nicht anlegbar (SD-Karte?)."); + running = false; return; } - static char tokens[0x50]; - snprintf(tokens, sizeof(tokens), "%s %s", at.accessToken[0] ? at.accessToken : "-", at.refreshToken); - memset(&at, 0, sizeof(at)); - - snprintf(why, sizeof(why), "Werkzeuge: Schritt 4/4 - Server fragen (HTTPS) ..."); - // Vom Server prüfen lassen (nur HTTPS, MBMNET-CA) - char body[32]; + // Geräte-ID nur zur Anzeige auf der Webseite (welche Konsole) + char devid[16] = ""; + u32 id = 0; + s32 internal = 0; + if (R_SUCCEEDED(amInit())) { + if (R_SUCCEEDED(AM_GetDeviceId(&internal, &id))) snprintf(devid, sizeof(devid), "%lu", (unsigned long)id); + amExit(); + } + char body[48]; u32 len = 0; - Result hrc = 0; - u32 code = https_request(server, "/mbmnet/admin-check", NULL, "X-NN-Tokens", tokens, body, sizeof(body), &len, NULL, 0, &hrc); - memset(tokens, 0, sizeof(tokens)); - isAdmin = code == 200 && strncmp(body, "admin=1", 7) == 0; - if (isAdmin) why[0] = 0; - else if (code == 200) snprintf(why, sizeof(why), "Werkzeuge gibt es nur für Admins."); - else if (code) snprintf(why, sizeof(why), "Werkzeuge: Server antwortet HTTP %lu.", (unsigned long)code); - else snprintf(why, sizeof(why), "Werkzeuge: keine HTTPS-Verbindung zum Server (%08lX).", (unsigned long)hrc); + Result rc = 0; + u32 code = https_request(server, "/mbmnet/admin-check", NULL, "X-Admin-Device", key, body, sizeof(body), &len, NULL, 0, &rc); + (void)devid; + if (code == 200 && !strncmp(body, "admin=1", 7)) { + isAdmin = true; + why[0] = 0; + } else if (code == 200 && !strncmp(body, "pair=", 5) && body[5]) { + char c[8]; + snprintf(c, sizeof(c), "%.6s", body + 5); + snprintf(why, sizeof(why), "Werkzeuge freischalten: Code %.3s %.3s\nauf mbmnet.mbm.wtf unter Admin > Konsolen eingeben,\n" + "dann nochmal SELECT drücken (10 Minuten gültig).", c, c + 3); + } else if (code == 200 && !strncmp(body, "pair=", 5)) { + snprintf(why, sizeof(why), "Werkzeuge: zu viele offene Freischaltungen, später nochmal."); + } else if (code == 200) { + snprintf(why, sizeof(why), "Werkzeuge: Diese Konsole ist nicht (mehr) freigeschaltet."); + } else if (code) { + snprintf(why, sizeof(why), "Werkzeuge: Server antwortet HTTP %lu.", (unsigned long)code); + } else { + snprintf(why, sizeof(why), "Werkzeuge: keine HTTPS-Verbindung zum Server (%08lX).", (unsigned long)rc); + } + running = false; } -const char *admin_why(void) { return why; } - -static bool checked; // Prüfung gelaufen: Ergebnis/Fehler in "why" nicht mehr überschreiben -void admin_progress(const char *step) { if (wanted && !isAdmin && !checked) snprintf(why, sizeof(why), "Werkzeuge: warte auf %s ...", step); } - -void admin_skipped(void) { if (wanted && !isAdmin) snprintf(why, sizeof(why), "Werkzeuge: Prüfung ausgefallen (umgeschaltet). App neu öffnen."); } - -void admin_prepare(const char *srv, bool mbmnetLoggedIn, u8 slot) +void admin_prepare(const char *srv, bool unused1, u8 unused2) { - mbmnetSlot = slot; + (void)unused1; (void)unused2; isAdmin = false; - wanted = mbmnetLoggedIn; - snprintf(why, sizeof(why), mbmnetLoggedIn ? "Werkzeuge: Server prüft noch ..." - : "Werkzeuge: nur bei aktivem MBMNET mit angemeldetem Admin-Konto."); remove(OLD_CACHE_FILE); snprintf(server, sizeof(server), "%s", srv); + snprintf(why, sizeof(why), "Werkzeuge: Server prüft noch ..."); } -void admin_check(void) +// Prüfen im Hintergrund (beim Start und bei SELECT, solange noch kein Admin). Läuft schon eine: nichts tun. +void admin_check_async(void) { - if (!wanted) return; - checked = true; - // act noch mit etwas anderem beschäftigt ("ungültiger Zustand"): kurz warten und noch einmal - for (int attempt = 0; attempt < 2; attempt++) { - check_once(); - if (isAdmin || !lastAct || ((lastAct >> 21) & 0x3F) != 5) break; // 5 = InvalidState - svcSleepThread(1000ULL * 1000 * 1000); - } + if (running || isAdmin) return; + if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; } + running = true; + s32 prio = 0x30; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + thread = threadCreate(check, NULL, 16 * 1024, prio + 1, -2, false); + if (!thread) running = false; } +void admin_check(void) { admin_check_async(); } bool admin_is(void) { return isAdmin; } +const char *admin_why(void) { return why; } +void admin_progress(const char *step) { (void)step; } +void admin_skipped(void) {} -bool admin_finish(void) { return true; } // kein eigener Thread mehr (läuft im Auffrischen) +bool admin_finish(void) +{ + if (!thread) return true; + if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false; // läuft noch + threadFree(thread); + thread = NULL; + return true; +} diff --git a/source/admin.h b/source/admin.h index 77dd000..0f861ea 100644 --- a/source/admin.h +++ b/source/admin.h @@ -16,3 +16,6 @@ bool admin_is(void); // Warum es (noch) keine Werkzeuge gibt (leer = Admin) const char *admin_why(void); bool admin_finish(void); // wartet höchstens 0,1 s, false = läuft noch + +// Prüfung im Hintergrund starten (beim Start und bei SELECT, solange noch keine Freigabe) +void admin_check_async(void); diff --git a/source/main.c b/source/main.c index 9c6f206..ef6e6da 100644 --- a/source/main.c +++ b/source/main.c @@ -351,8 +351,6 @@ static void maintenance_worker(void *arg) char ignore[96]; // 1. Patches auffrischen (Miiverse-CA aus dem Zwischenspeicher; nur beim allerersten Mal vom Server) bool hadCa = olvpatch_ca_cached(); - // 0. Admin? Zuerst, damit die Werkzeuge nicht vom Rest abhängen (act ist hier noch frei) - if (!switchRequested) admin_check(); patch_progress = maint_step; maint_step("Patches"); refresh_patches(); @@ -559,17 +557,28 @@ static bool run(int action, char *msg, size_t msglen) case BTN_DISABLE: return disable(confirmed, msg, msglen); case BTN_DIAG: patch_diagnose(msg, msglen); return false; case BTN_UNLINK: { - // Verknüpfte NNID nur auf dem 3DS lösen (ohne Server) - z. B. wenn das Konto gelöscht wurde + // Verknüpfte ID nur auf dem 3DS lösen (ohne Server). Bei aktivem MBMNET gezielt den MBMNET-Platz - + // nicht den, auf dem act gerade steht (der war nach einem Soft-Reboot manchmal noch der Pretendo-Platz). char id[20]; - if (!nnid_linked(id, sizeof(id))) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; } + bool mbmnet = patch_state() == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET; + u8 slot = mbmnet ? frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET) : 0; + bool linked = slot ? frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)) : nnid_linked(id, sizeof(id)); + if (!linked) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; } if (!confirmed) { armed = BTN_UNLINK; - snprintf(msg, msglen, "\"%s\" vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n" - "Zum Bestätigen nochmal Y drücken.", id); + snprintf(msg, msglen, "\"%s\"%s vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n" + "Zum Bestätigen nochmal Y drücken.", id, slot ? " (MBMNET-Platz)" : ""); return false; } - if (!nnid_unlink(msg, msglen)) return false; - snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach kannst du eine andere ID verknüpfen.", id); + if (slot ? !nnid_unlink_slot(slot, msg, msglen) : !nnid_unlink(msg, msglen)) return false; + if (slot) { + char hint[160]; + remember_mbmnet_id(id); + link_hint(hint, sizeof(hint)); + snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach neu verknüpfen:\n%s", id, hint); + } else { + snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach kannst du eine andere ID verknüpfen.", id); + } return true; } case BTN_FTP: @@ -656,6 +665,7 @@ int main(int argc, char **argv) u8 mslot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET); admin_prepare(server, st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET && mslot && frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, mid, sizeof(mid)), mslot); + admin_check_async(); // Freigabe der Konsole beim Server erfragen (Hintergrund) } // Ein SpotPass-Test von früher (App mitten im Test verlassen) darf nicht weiterlaufen spotpass_test_cancel(); @@ -685,6 +695,7 @@ int main(int argc, char **argv) int selected = st == STATE_MBMNET ? BTN_DISABLE : BTN_ENABLE; bool toolsToggled = false; + bool showWhy = false; // Meldung zur Freischaltung der Werkzeuge anzeigen (nach SELECT) int queued = -1; // Aktion, die nach dem nächsten Bild läuft (damit sofort etwas passiert) while (aptMainLoop()) { @@ -699,7 +710,13 @@ int main(int argc, char **argv) } bool wasTools = toolsToggled; if (down & KEY_SELECT && admin_is()) toolsToggled = !toolsToggled; - else if (down & KEY_SELECT) snprintf(msg, sizeof(msg), "%s\n(Auffrischen: %s)", admin_why(), maintStep); + else if (down & KEY_SELECT) { + admin_check_async(); // nach dem Freischalten auf der Webseite: gleich nochmal fragen + showWhy = true; + } + // Stand der Freischaltung live anzeigen, bis etwas anderes passiert + if (showWhy && (admin_is() || down & (KEY_A | KEY_B | KEY_Y | KEY_TOUCH))) { showWhy = false; if (admin_is()) msg[0] = 0; } + if (showWhy) snprintf(msg, sizeof(msg), "%s", admin_why()); if (down & KEY_B && toolsToggled) toolsToggled = false; if (!admin_is()) toolsToggled = false; // Werkzeuge nur für Admins (Server hat es bestätigt) // Werkzeuge nur mit SELECT, beim Öffnen immer aus