MBMNET-App v2.8.2: Quellcode (GPLv3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:05:44 +02:00
co-authored by Claude Opus 5.5
commit d264cd93cd
42 changed files with 7054 additions and 0 deletions
+335
View File
@@ -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);
}
}