336 lines
14 KiB
C
336 lines
14 KiB
C
// 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);
|
|
}
|
|
}
|