MBMNET-App v2.8.2: Quellcode (GPLv3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
// FTP-Kern des MBMNET-FTP-Moduls (Hintergrund-Prozess, siehe main.c).
|
||||
//
|
||||
// - Ohne Benutzername/Passwort (jede Anmeldung wird angenommen), nur Passiv-Modus (PASV/EPSV).
|
||||
// - Lesen: ganze SD-Karte. Schreiben/Löschen/Ordner anlegen: nur unter /3ds/mbmnet, damit niemand im
|
||||
// WLAN Luma oder boot.firm überschreiben kann.
|
||||
// - Eine Verbindung nach der anderen.
|
||||
#include <3ds.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <poll.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ftp.h"
|
||||
|
||||
#define WRITE_ROOT "/3ds/mbmnet"
|
||||
#define XFER_SIZE (32 * 1024)
|
||||
|
||||
|
||||
// --- Hilfen ---------------------------------------------------------------------------
|
||||
|
||||
static void reply(int fd, const char *fmt, ...)
|
||||
{
|
||||
char line[512];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int n = vsnprintf(line, sizeof(line) - 2, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) return;
|
||||
if (n > (int)sizeof(line) - 3) n = sizeof(line) - 3;
|
||||
line[n++] = '\r';
|
||||
line[n++] = '\n';
|
||||
send(fd, line, n, 0);
|
||||
}
|
||||
|
||||
static bool send_all(int fd, const char *buf, size_t len)
|
||||
{
|
||||
while (len) {
|
||||
ssize_t n = send(fd, buf, len, 0);
|
||||
if (n <= 0) return false;
|
||||
buf += n;
|
||||
len -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pfad auflösen: absolut oder relativ zu cwd, ".." und "." entfernen. Ergebnis beginnt mit "/".
|
||||
static void resolve(const char *cwd, const char *arg, char *out, size_t len)
|
||||
{
|
||||
char tmp[512];
|
||||
if (!arg || !arg[0]) snprintf(tmp, sizeof(tmp), "%s", cwd);
|
||||
else if (arg[0] == '/') snprintf(tmp, sizeof(tmp), "%s", arg);
|
||||
else snprintf(tmp, sizeof(tmp), "%s/%s", strcmp(cwd, "/") ? cwd : "", arg);
|
||||
|
||||
char *parts[64];
|
||||
int count = 0;
|
||||
for (char *p = strtok(tmp, "/"); p && count < 64; p = strtok(NULL, "/")) {
|
||||
if (!strcmp(p, ".")) continue;
|
||||
if (!strcmp(p, "..")) { if (count) count--; continue; }
|
||||
parts[count++] = p;
|
||||
}
|
||||
size_t o = 0;
|
||||
out[0] = 0;
|
||||
for (int i = 0; i < count && o + 2 < len; i++) o += snprintf(out + o, len - o, "/%s", parts[i]);
|
||||
if (!count) snprintf(out, len, "/");
|
||||
}
|
||||
|
||||
static void sd_path(const char *path, char *out, size_t len) { snprintf(out, len, "sdmc:%s", path); }
|
||||
|
||||
static bool writable(const char *path)
|
||||
{
|
||||
size_t n = strlen(WRITE_ROOT);
|
||||
return !strncmp(path, WRITE_ROOT, n) && (path[n] == 0 || path[n] == '/');
|
||||
}
|
||||
|
||||
// --- Daten-Verbindung (passiv) ----------------------------------------------------------
|
||||
|
||||
static int open_passive(int ctrl, bool extended)
|
||||
{
|
||||
static int nextPort = CONTROL_PORT + 1;
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY };
|
||||
for (int tries = 0; tries < 50; tries++) {
|
||||
int port = nextPort++;
|
||||
if (nextPort > CONTROL_PORT + 200) nextPort = CONTROL_PORT + 1;
|
||||
addr.sin_port = htons(port);
|
||||
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0 && listen(fd, 1) == 0) {
|
||||
u32 ip = (u32)gethostid();
|
||||
if (extended) reply(ctrl, "229 Entering Extended Passive Mode (|||%d|)", port);
|
||||
else reply(ctrl, "227 Entering Passive Mode (%lu,%lu,%lu,%lu,%d,%d)", (unsigned long)(ip & 0xFF),
|
||||
(unsigned long)((ip >> 8) & 0xFF), (unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24),
|
||||
port >> 8, port & 0xFF);
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Wartet auf den Client (höchstens 10 s) und gibt die Daten-Verbindung zurück
|
||||
static int accept_data(int pasvFd)
|
||||
{
|
||||
struct pollfd p = { .fd = pasvFd, .events = POLLIN };
|
||||
if (poll(&p, 1, 10000) <= 0) return -1;
|
||||
return accept(pasvFd, NULL, NULL);
|
||||
}
|
||||
|
||||
// --- Befehle -----------------------------------------------------------------------------
|
||||
|
||||
// Ein Eintrag im MLSD/MLST-Format (maschinenlesbar, wie FileZilla es abfragt)
|
||||
static int mlsx_fact(char *out, size_t len, const struct stat *st, bool dir, const char *name)
|
||||
{
|
||||
char modify[20] = "20000101000000";
|
||||
if (st->st_mtime > 0) strftime(modify, sizeof(modify), "%Y%m%d%H%M%S", gmtime(&st->st_mtime));
|
||||
return snprintf(out, len, "type=%s;size=%llu;modify=%s;perm=%s; %s\r\n", dir ? "dir" : "file",
|
||||
(unsigned long long)(dir ? 0 : st->st_size), modify, dir ? "el" : "r", name);
|
||||
}
|
||||
|
||||
enum { LIST_UNIX, LIST_NAMES, LIST_MLSD };
|
||||
|
||||
static void list_dir(int ctrl, int data, const char *path, int mode)
|
||||
{
|
||||
char sd[600];
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
DIR *d = opendir(sd);
|
||||
if (!d) { reply(ctrl, "550 Ordner nicht gefunden"); return; }
|
||||
reply(ctrl, "150 Liste folgt");
|
||||
struct dirent *e;
|
||||
char line[700], child[900];
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
if (mode == LIST_NAMES) { int n = snprintf(line, sizeof(line), "%s\r\n", e->d_name); send_all(data, line, n); continue; }
|
||||
snprintf(child, sizeof(child), "%s/%s", sd, e->d_name);
|
||||
struct stat st = { 0 };
|
||||
stat(child, &st);
|
||||
bool dir = S_ISDIR(st.st_mode) || e->d_type == DT_DIR;
|
||||
if (mode == LIST_MLSD) {
|
||||
int n = mlsx_fact(line, sizeof(line), &st, dir, e->d_name);
|
||||
if (!send_all(data, line, n)) break;
|
||||
continue;
|
||||
}
|
||||
char date[16] = "Jan 1 2000";
|
||||
if (st.st_mtime > 0) strftime(date, sizeof(date), "%b %d %H:%M", localtime(&st.st_mtime));
|
||||
int n = snprintf(line, sizeof(line), "%s 1 3ds 3ds %10llu %s %s\r\n", dir ? "drwxr-xr-x" : "-rw-r--r--",
|
||||
(unsigned long long)(dir ? 0 : st.st_size), date, e->d_name);
|
||||
if (!send_all(data, line, n)) break;
|
||||
}
|
||||
closedir(d);
|
||||
reply(ctrl, "226 Fertig");
|
||||
}
|
||||
|
||||
static void retrieve(int ctrl, int data, const char *path)
|
||||
{
|
||||
char sd[600];
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
FILE *f = fopen(sd, "rb");
|
||||
if (!f) { reply(ctrl, "550 Datei nicht gefunden"); return; }
|
||||
reply(ctrl, "150 Sende Datei");
|
||||
char *buf = malloc(XFER_SIZE);
|
||||
bool ok = buf != NULL;
|
||||
size_t n;
|
||||
while (ok && (n = fread(buf, 1, XFER_SIZE, f)) > 0) ok = send_all(data, buf, n);
|
||||
free(buf);
|
||||
fclose(f);
|
||||
reply(ctrl, ok ? "226 Fertig" : "426 Übertragung abgebrochen");
|
||||
}
|
||||
|
||||
static void store(int ctrl, int data, const char *path)
|
||||
{
|
||||
char sd[600];
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
FILE *f = fopen(sd, "wb");
|
||||
if (!f) { reply(ctrl, "550 Datei nicht schreibbar"); return; }
|
||||
reply(ctrl, "150 Empfange Datei");
|
||||
char *buf = malloc(XFER_SIZE);
|
||||
bool ok = buf != NULL;
|
||||
ssize_t n;
|
||||
while (ok && (n = recv(data, buf, XFER_SIZE, 0)) > 0) ok = fwrite(buf, 1, n, f) == (size_t)n;
|
||||
free(buf);
|
||||
fclose(f);
|
||||
reply(ctrl, ok ? "226 Fertig" : "451 Schreiben fehlgeschlagen");
|
||||
}
|
||||
|
||||
static void session(int ctrl)
|
||||
{
|
||||
char cwd[512] = "/", line[600], path[512], sd[600], renameFrom[512] = "";
|
||||
int pasvFd = -1;
|
||||
size_t used = 0;
|
||||
reply(ctrl, "220 MBMNET-FTP (ohne Passwort, Schreiben nur in " WRITE_ROOT ")");
|
||||
|
||||
while (!ftp_should_exit()) {
|
||||
// Eine Zeile lesen
|
||||
char *eol;
|
||||
while (!(eol = memchr(line, '\n', used))) {
|
||||
struct pollfd p = { .fd = ctrl, .events = POLLIN };
|
||||
if (ftp_should_exit()) goto done;
|
||||
int r = poll(&p, 1, 500);
|
||||
if (r < 0) goto done;
|
||||
if (r == 0) continue;
|
||||
if (used >= sizeof(line) - 1) used = 0; // zu lange Zeile verwerfen
|
||||
ssize_t n = recv(ctrl, line + used, sizeof(line) - 1 - used, 0);
|
||||
if (n <= 0) goto done;
|
||||
used += n;
|
||||
}
|
||||
*eol = 0;
|
||||
if (eol > line && eol[-1] == '\r') eol[-1] = 0;
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "%s", line);
|
||||
size_t rest = used - (eol - line + 1);
|
||||
memmove(line, eol + 1, rest);
|
||||
used = rest;
|
||||
|
||||
char *arg = strchr(cmd, ' ');
|
||||
if (arg) *arg++ = 0;
|
||||
for (char *c = cmd; *c; c++) if (*c >= 'a' && *c <= 'z') *c -= 32;
|
||||
|
||||
if (!strcmp(cmd, "USER")) reply(ctrl, "331 Passwort egal");
|
||||
else if (!strcmp(cmd, "PASS")) reply(ctrl, "230 Angemeldet");
|
||||
else if (!strcmp(cmd, "SYST")) reply(ctrl, "215 UNIX Type: L8");
|
||||
else if (!strcmp(cmd, "FEAT")) { reply(ctrl, "211-Features:\r\n PASV\r\n EPSV\r\n SIZE\r\n MDTM\r\n MLST type*;size*;modify*;perm*;\r\n UTF8\r\n211 Ende"); }
|
||||
else if (!strcmp(cmd, "OPTS")) reply(ctrl, "200 OK");
|
||||
else if (!strcmp(cmd, "NOOP")) reply(ctrl, "200 OK");
|
||||
else if (!strcmp(cmd, "TYPE") || !strcmp(cmd, "MODE") || !strcmp(cmd, "STRU")) reply(ctrl, "200 OK");
|
||||
else if (!strcmp(cmd, "PWD") || !strcmp(cmd, "XPWD")) reply(ctrl, "257 \"%s\"", cwd);
|
||||
else if (!strcmp(cmd, "CWD") || !strcmp(cmd, "CDUP")) {
|
||||
resolve(cwd, !strcmp(cmd, "CDUP") ? ".." : arg, path, sizeof(path));
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
struct stat st;
|
||||
if (stat(sd, &st) == 0 && S_ISDIR(st.st_mode)) { snprintf(cwd, sizeof(cwd), "%s", path); reply(ctrl, "250 OK"); }
|
||||
else if (!strcmp(path, "/")) { snprintf(cwd, sizeof(cwd), "/"); reply(ctrl, "250 OK"); }
|
||||
else reply(ctrl, "550 Ordner nicht gefunden");
|
||||
}
|
||||
else if (!strcmp(cmd, "PASV") || !strcmp(cmd, "EPSV")) {
|
||||
if (pasvFd >= 0) close(pasvFd);
|
||||
pasvFd = open_passive(ctrl, !strcmp(cmd, "EPSV"));
|
||||
if (pasvFd < 0) reply(ctrl, "425 Kein Daten-Port frei");
|
||||
}
|
||||
else if (!strcmp(cmd, "SIZE")) {
|
||||
resolve(cwd, arg, path, sizeof(path));
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
struct stat st;
|
||||
if (stat(sd, &st) == 0 && !S_ISDIR(st.st_mode)) reply(ctrl, "213 %llu", (unsigned long long)st.st_size);
|
||||
else reply(ctrl, "550 Nicht gefunden");
|
||||
}
|
||||
else if (!strcmp(cmd, "MLST")) {
|
||||
// Ein einzelner Eintrag, direkt auf der Steuer-Verbindung
|
||||
resolve(cwd, arg, path, sizeof(path));
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
struct stat st = { 0 };
|
||||
bool root = !strcmp(path, "/");
|
||||
if (!root && stat(sd, &st) != 0) { reply(ctrl, "550 Nicht gefunden"); continue; }
|
||||
char fact[700];
|
||||
mlsx_fact(fact, sizeof(fact), &st, root || S_ISDIR(st.st_mode), path);
|
||||
fact[strcspn(fact, "\r\n")] = 0;
|
||||
reply(ctrl, "250-Eintrag\r\n %s\r\n250 Ende", fact);
|
||||
}
|
||||
else if (!strcmp(cmd, "MDTM")) {
|
||||
resolve(cwd, arg, path, sizeof(path));
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
struct stat st;
|
||||
char modify[20] = "20000101000000";
|
||||
if (stat(sd, &st) != 0) { reply(ctrl, "550 Nicht gefunden"); continue; }
|
||||
if (st.st_mtime > 0) strftime(modify, sizeof(modify), "%Y%m%d%H%M%S", gmtime(&st.st_mtime));
|
||||
reply(ctrl, "213 %s", modify);
|
||||
}
|
||||
else if (!strcmp(cmd, "LIST") || !strcmp(cmd, "NLST") || !strcmp(cmd, "MLSD") || !strcmp(cmd, "RETR") || !strcmp(cmd, "STOR")) {
|
||||
bool isList = cmd[0] == 'L' || cmd[0] == 'N' || cmd[0] == 'M';
|
||||
// "LIST -la" u. ä.: Optionen ignorieren
|
||||
const char *target = arg && arg[0] == '-' ? NULL : arg;
|
||||
resolve(cwd, target, path, sizeof(path));
|
||||
if (!strcmp(cmd, "STOR") && !writable(path)) { reply(ctrl, "550 Schreiben nur in " WRITE_ROOT); continue; }
|
||||
if (pasvFd < 0) { reply(ctrl, "425 Erst PASV senden"); continue; }
|
||||
int data = accept_data(pasvFd);
|
||||
close(pasvFd);
|
||||
pasvFd = -1;
|
||||
if (data < 0) { reply(ctrl, "425 Keine Daten-Verbindung"); continue; }
|
||||
if (isList) list_dir(ctrl, data, path, cmd[0] == 'N' ? LIST_NAMES : cmd[0] == 'M' ? LIST_MLSD : LIST_UNIX);
|
||||
else if (cmd[0] == 'R') retrieve(ctrl, data, path);
|
||||
else store(ctrl, data, path);
|
||||
close(data);
|
||||
}
|
||||
else if (!strcmp(cmd, "DELE") || !strcmp(cmd, "RMD") || !strcmp(cmd, "MKD") || !strcmp(cmd, "XMKD")) {
|
||||
resolve(cwd, arg, path, sizeof(path));
|
||||
if (!writable(path) || !strcmp(path, WRITE_ROOT)) { reply(ctrl, "550 Ändern nur in " WRITE_ROOT); continue; }
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
int r = !strcmp(cmd, "DELE") ? remove(sd) : !strcmp(cmd, "RMD") ? rmdir(sd) : mkdir(sd, 0777);
|
||||
if (r == 0) reply(ctrl, cmd[0] == 'M' || cmd[0] == 'X' ? "257 \"%s\" angelegt" : "250 OK", path);
|
||||
else reply(ctrl, "550 Fehlgeschlagen");
|
||||
}
|
||||
else if (!strcmp(cmd, "RNFR")) {
|
||||
resolve(cwd, arg, renameFrom, sizeof(renameFrom));
|
||||
if (!writable(renameFrom)) { renameFrom[0] = 0; reply(ctrl, "550 Ändern nur in " WRITE_ROOT); }
|
||||
else reply(ctrl, "350 Weiter mit RNTO");
|
||||
}
|
||||
else if (!strcmp(cmd, "RNTO")) {
|
||||
resolve(cwd, arg, path, sizeof(path));
|
||||
char from[600];
|
||||
sd_path(renameFrom, from, sizeof(from));
|
||||
sd_path(path, sd, sizeof(sd));
|
||||
if (renameFrom[0] && writable(path) && rename(from, sd) == 0) reply(ctrl, "250 OK");
|
||||
else reply(ctrl, "550 Umbenennen fehlgeschlagen");
|
||||
renameFrom[0] = 0;
|
||||
}
|
||||
else if (!strcmp(cmd, "QUIT")) { reply(ctrl, "221 Tschüss"); break; }
|
||||
else reply(ctrl, "502 Nicht unterstützt");
|
||||
}
|
||||
done:
|
||||
if (pasvFd >= 0) close(pasvFd);
|
||||
}
|
||||
|
||||
// Nimmt Verbindungen an, bis das Netz weg ist, sich die IP ändert oder das Modul aufhören soll.
|
||||
void ftp_serve(int listenFd, u32 ip)
|
||||
{
|
||||
while (!ftp_should_exit()) {
|
||||
if ((u32)gethostid() != ip) return; // WLAN gewechselt oder weg
|
||||
struct pollfd p = { .fd = listenFd, .events = POLLIN };
|
||||
int r = poll(&p, 1, 2000);
|
||||
if (r < 0 || (p.revents & (POLLERR | POLLHUP | POLLNVAL))) return;
|
||||
if (r == 0) continue;
|
||||
int ctrl = accept(listenFd, NULL, NULL);
|
||||
if (ctrl < 0) return;
|
||||
session(ctrl);
|
||||
close(ctrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <3ds/types.h>
|
||||
|
||||
#define CONTROL_PORT 5000
|
||||
|
||||
// Nimmt FTP-Verbindungen an, bis das Netz weg ist oder sich die IP ändert
|
||||
void ftp_serve(int listenFd, u32 ip);
|
||||
|
||||
// Soll das Modul aufhören? (Stopp-Datei der App oder Beenden-Aufforderung des Systems; in main.c)
|
||||
bool ftp_should_exit(void);
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
// Statusblock des FTP-Moduls. Die App liest ihn per Debug-Zugriff direkt aus dem Speicher des Moduls
|
||||
// (die Adresse steht beim Bauen fest) - so klappt die Anzeige auch, wenn das Modul nicht an die SD kommt.
|
||||
// Über "stop" kann die App das Modul ebenfalls ohne SD zum Beenden auffordern.
|
||||
// Wird von ftpmodul/source/main.c und source/ftpctl.c (App) benutzt.
|
||||
#include <3ds/types.h>
|
||||
|
||||
#define FTP_STATUS_MAGIC 0x5446424D // "MBFT"
|
||||
|
||||
enum {
|
||||
FTP_STAGE_START = 0, // vor __appInit / srvInit
|
||||
FTP_STAGE_INIT, // srv, fs, SD erledigt (Ergebnisse in srv/fs/sdmc)
|
||||
FTP_STAGE_SOC, // wartet auf den Netzwerk-Dienst (socInit, Ergebnis in soc)
|
||||
FTP_STAGE_WIFI, // wartet auf WLAN (keine IP)
|
||||
FTP_STAGE_PORT, // Port 5000 lässt sich nicht öffnen (errno in bindErrno)
|
||||
FTP_STAGE_READY, // bereit, IP in ip
|
||||
FTP_STAGE_EXIT, // beendet sich
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
u32 magic;
|
||||
u32 stage;
|
||||
s32 srv, fs, sdmc, soc;
|
||||
u32 ip;
|
||||
u32 heartbeat; // zählt hoch, solange das Modul lebt
|
||||
u32 stop; // die App setzt 1: Modul beendet sich sauber
|
||||
s32 bindErrno;
|
||||
} FtpStatus;
|
||||
@@ -0,0 +1,181 @@
|
||||
// MBMNET-FTP-Modul: läuft als eigener Hintergrund-Prozess, auch wenn die MBMNET-App geschlossen ist.
|
||||
//
|
||||
// Die App legt dieses Modul als /luma/sysmodules/0004013000F1E002.cxi ab und startet es über den
|
||||
// Prozess-Manager (Luma lädt Systemmodule mit dieser Kennung von der SD). "FTP aus" in der App
|
||||
// beendet den Prozess. Nach einem Neustart des 3DS ist es aus, bis die App es wieder startet.
|
||||
//
|
||||
// Die aktuelle Adresse steht für die App in /3ds/mbmnet/ftp-adresse.txt.
|
||||
//
|
||||
// Beenden immer sauber durch das Modul selbst (nie von außen abschießen - das brachte den 3DS zum
|
||||
// Absturz): "FTP aus" legt /3ds/mbmnet/ftp-stopp an, beim Neustart/Ausschalten schickt das System
|
||||
// die Benachrichtigung 0x100. Dann Verbindungen und Netzwerk-Dienst schließen und zurückkehren.
|
||||
#include <3ds.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <malloc.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ftp.h"
|
||||
#include "ftpstatus.h"
|
||||
|
||||
// Statusblock für die App (Adresse fest, siehe Makefile -> ftpmodul.sym)
|
||||
volatile FtpStatus ftpStatus __attribute__((used, aligned(4))) = { .magic = FTP_STATUS_MAGIC };
|
||||
|
||||
#define SOC_BUFFER_SIZE 0x20000
|
||||
#define ADDRESS_FILE "sdmc:/3ds/mbmnet/ftp-adresse.txt"
|
||||
#define LOG_FILE "sdmc:/3ds/mbmnet/ftp.log"
|
||||
#define STOP_FILE "sdmc:/3ds/mbmnet/ftp-stopp"
|
||||
|
||||
static Handle notificationSemaphore;
|
||||
static bool quit;
|
||||
|
||||
|
||||
// Protokoll für die Fehlersuche (die App zeigt die letzte Zeile, wenn der Server nicht antwortet)
|
||||
static void logf_(const char *fmt, ...)
|
||||
{
|
||||
FILE *f = fopen(LOG_FILE, "a");
|
||||
if (!f) return;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(f, fmt, ap);
|
||||
va_end(ap);
|
||||
fputc('\n', f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// Ein Hintergrund-Modul bekommt nur wenig Speicher: Heap fest vorgeben statt "alles, was frei ist"
|
||||
u32 __ctru_heap_size = 0x80000;
|
||||
u32 __ctru_linear_heap_size = 0x1000;
|
||||
|
||||
// Nur die Dienste, die das Modul braucht (kein Grafik-/HOME-Menü-Dienst wie bei einer App)
|
||||
void __appInit(void)
|
||||
{
|
||||
// srv ist beim Start evtl. noch belegt: kurz wiederholen
|
||||
for (int i = 0; i < 100 && R_FAILED(ftpStatus.srv = srvInit()); i++) svcSleepThread(50 * 1000 * 1000);
|
||||
ftpStatus.fs = fsInit();
|
||||
ftpStatus.sdmc = archiveMountSdmc();
|
||||
ftpStatus.stage = FTP_STAGE_INIT;
|
||||
}
|
||||
|
||||
void __appExit(void)
|
||||
{
|
||||
archiveUnmountAll();
|
||||
fsExit();
|
||||
srvExit();
|
||||
}
|
||||
|
||||
bool ftp_should_exit(void)
|
||||
{
|
||||
ftpStatus.heartbeat++;
|
||||
if (ftpStatus.stop) quit = true; // Stopp-Bit der App (geht auch ohne SD)
|
||||
if (quit) return true;
|
||||
// Beenden-Aufforderung des Systems (Neustart, Ausschalten)
|
||||
if (notificationSemaphore && R_SUCCEEDED(svcWaitSynchronization(notificationSemaphore, 0))) {
|
||||
u32 id = 0;
|
||||
if (R_SUCCEEDED(srvReceiveNotification(&id)) && id == 0x100) quit = true;
|
||||
}
|
||||
// Stopp-Datei der App, höchstens alle 2 Sekunden nachsehen
|
||||
static u64 lastCheck;
|
||||
u64 now = osGetTime();
|
||||
if (!quit && now - lastCheck >= 2000) {
|
||||
lastCheck = now;
|
||||
struct stat st;
|
||||
if (stat(STOP_FILE, &st) == 0) quit = true;
|
||||
}
|
||||
return quit;
|
||||
}
|
||||
|
||||
// Wartet ms Millisekunden, bricht aber ab, sobald das Modul aufhören soll
|
||||
static void sleep_ms(int ms)
|
||||
{
|
||||
for (int waited = 0; waited < ms && !ftp_should_exit(); waited += 100) svcSleepThread(100LL * 1000 * 1000);
|
||||
}
|
||||
|
||||
static void write_address(u32 ip)
|
||||
{
|
||||
mkdir("sdmc:/3ds", 0777);
|
||||
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||
FILE *f = fopen(ADDRESS_FILE, "w");
|
||||
if (!f) return;
|
||||
if (ip) fprintf(f, "%lu.%lu.%lu.%lu:%d\n", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
|
||||
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
remove(LOG_FILE);
|
||||
remove(STOP_FILE);
|
||||
srvEnableNotification(¬ificationSemaphore); // für die Beenden-Aufforderung 0x100
|
||||
logf_("MBMNET-FTP-Modul gestartet: srv %08lX, fs %08lX, sdmc %08lX, frei (Base) %lld KB",
|
||||
(unsigned long)ftpStatus.srv, (unsigned long)ftpStatus.fs, (unsigned long)ftpStatus.sdmc,
|
||||
osGetMemRegionFree(MEMREGION_BASE) / 1024);
|
||||
u32 *socBuffer = memalign(0x1000, SOC_BUFFER_SIZE);
|
||||
if (!socBuffer) { logf_("Fehler: kein Speicher für den Netzwerk-Puffer"); return 1; }
|
||||
|
||||
while (!ftp_should_exit()) {
|
||||
// Netzwerk-Dienst (nach dem Schlafmodus ggf. neu)
|
||||
Result rc;
|
||||
int tries = 0;
|
||||
ftpStatus.stage = FTP_STAGE_SOC;
|
||||
while (R_FAILED(rc = ftpStatus.soc = socInit(socBuffer, SOC_BUFFER_SIZE))) {
|
||||
if (ftp_should_exit()) goto done;
|
||||
if (tries++ % 15 == 0) logf_("Netzwerk-Dienst (socInit) fehlgeschlagen: %08lX", (unsigned long)rc);
|
||||
sleep_ms(2000);
|
||||
}
|
||||
logf_("Netzwerk-Dienst bereit");
|
||||
|
||||
int failures = 0, waits = 0;
|
||||
while (failures < 5 && !ftp_should_exit()) {
|
||||
u32 ip = (u32)gethostid();
|
||||
ftpStatus.ip = ip;
|
||||
if (!ip) { // noch kein WLAN
|
||||
ftpStatus.stage = FTP_STAGE_WIFI;
|
||||
if (waits++ % 15 == 0) logf_("Warte auf WLAN (keine IP)");
|
||||
write_address(0);
|
||||
sleep_ms(2000);
|
||||
continue;
|
||||
}
|
||||
waits = 0;
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(CONTROL_PORT), .sin_addr.s_addr = INADDR_ANY };
|
||||
if (fd < 0 || bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 2) != 0) {
|
||||
logf_("Port %d öffnen fehlgeschlagen (Socket %d, errno %d)", CONTROL_PORT, fd, errno);
|
||||
ftpStatus.stage = FTP_STAGE_PORT;
|
||||
ftpStatus.bindErrno = errno;
|
||||
if (fd >= 0) close(fd);
|
||||
failures++;
|
||||
sleep_ms(2000);
|
||||
continue;
|
||||
}
|
||||
failures = 0;
|
||||
ftpStatus.stage = FTP_STAGE_READY;
|
||||
write_address(ip);
|
||||
logf_("Bereit: ftp://%lu.%lu.%lu.%lu:%d", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
|
||||
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
|
||||
ftp_serve(fd, ip); // kehrt zurück, wenn das Netz weg ist oder sich die IP ändert
|
||||
close(fd);
|
||||
sleep_ms(500);
|
||||
}
|
||||
socExit();
|
||||
if (ftp_should_exit()) break;
|
||||
// Mehrfach kein Socket möglich (z. B. nach dem Schlafmodus): Netzwerk-Dienst neu aufsetzen
|
||||
logf_("Netzwerk-Dienst wird neu aufgesetzt");
|
||||
sleep_ms(2000);
|
||||
}
|
||||
done:
|
||||
ftpStatus.stage = FTP_STAGE_EXIT;
|
||||
// Sauber beenden: Adresse weg, Stopp-Datei weg; libctru schließt danach fs und srv
|
||||
remove(ADDRESS_FILE);
|
||||
remove(STOP_FILE);
|
||||
logf_("Beendet");
|
||||
free(socBuffer);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user