// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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); } }