/ firmware / src / filesystems / api.cpp
api.cpp
  1  #include "api.h"
  2  
  3  #include <storage.h>
  4  
  5  #include <LittleFS.h>
  6  #include <SD.h>
  7  
  8  bool filesystems::api::isSensitivePath(const String &path) {
  9    return path == "/.ssh" || path == ".ssh"
 10        || path.startsWith("/.ssh/") || path.startsWith(".ssh/")
 11        || path == config::ssh::HOSTKEY_PATH;
 12  }
 13  
 14  bool filesystems::api::resolveTarget(FilesystemResolveCommand *command) {
 15    if (!command) return false;
 16    command->target = {nullptr, "", false};
 17  
 18    const char *prefix = "/api/filesystem/";
 19    String remainder = command->url.substring(strlen(prefix));
 20  
 21    if (remainder.startsWith("sd")) {
 22      String path = remainder.substring(2);
 23      if (path.isEmpty()) path = "/";
 24      if (hardware::storage::ensureSD()) {
 25        command->target = {&SD, path, true};
 26        return true;
 27      }
 28      command->target = {nullptr, path, false};
 29      return false;
 30    }
 31  
 32    if (remainder.startsWith("littlefs")) {
 33      String path = remainder.substring(8);
 34      if (path.isEmpty()) path = "/";
 35      if (hardware::storage::ensureLittleFS()) {
 36        command->target = {&LittleFS, path, true};
 37        return true;
 38      }
 39      command->target = {nullptr, path, false};
 40      return false;
 41    }
 42  
 43    return false;
 44  }
 45  
 46  void filesystems::api::listDirectory(fs::FS &filesystem, const String &path, JsonArray &out) {
 47    File dir = filesystem.open(path);
 48    if (!dir || !dir.isDirectory()) return;
 49  
 50    File entry = dir.openNextFile();
 51    while (entry) {
 52      String name = String(entry.name());
 53      if (!filesystems::api::isSensitivePath(name)) {
 54        JsonObject object = out.add<JsonObject>();
 55        object["name"] = name;
 56        object["size"] = (unsigned long long)entry.size();
 57        object["dir"] = entry.isDirectory();
 58        object["last_write_unix"] = (unsigned long long)entry.getLastWrite();
 59      }
 60      entry = dir.openNextFile();
 61    }
 62  
 63    dir.close();
 64  }
 65  
 66  bool filesystems::api::removeRecursive(fs::FS &filesystem, const String &path) {
 67    File entry = filesystem.open(path);
 68    if (!entry) return false;
 69  
 70    if (!entry.isDirectory()) {
 71      entry.close();
 72      return filesystem.remove(path);
 73    }
 74  
 75    entry.close();
 76    File dir = filesystem.open(path);
 77    File child = dir.openNextFile();
 78    while (child) {
 79      String child_name = String(child.name());
 80      bool is_directory = child.isDirectory();
 81      child.close();
 82      String child_path = (path == "/") ? "/" + child_name : path + "/" + child_name;
 83  
 84      if (is_directory) {
 85        if (!filesystems::api::removeRecursive(filesystem, child_path)) {
 86          dir.close();
 87          return false;
 88        }
 89      } else {
 90        if (!filesystem.remove(child_path)) {
 91          dir.close();
 92          return false;
 93        }
 94      }
 95  
 96      child = dir.openNextFile();
 97    }
 98  
 99    dir.close();
100    return filesystem.rmdir(path);
101  }
102  
103  #ifdef PIO_UNIT_TESTING
104  
105  #include <testing/utils.h>
106  
107  namespace filesystems::api { void test(void); }
108  
109  static void test_api_sensitive_path_with_slash(void) {
110    GIVEN("a path starting with /.ssh");
111    THEN("it is detected as sensitive");
112    TEST_ASSERT_TRUE_MESSAGE(filesystems::api::isSensitivePath("/.ssh"),
113      "device: /.ssh must be detected as sensitive");
114  }
115  
116  static void test_api_sensitive_path_without_slash(void) {
117    GIVEN("a path .ssh without leading slash");
118    THEN("it is detected as sensitive");
119    TEST_ASSERT_TRUE_MESSAGE(filesystems::api::isSensitivePath(".ssh"),
120      "device: .ssh without leading slash must be sensitive");
121  }
122  
123  static void test_api_sensitive_path_nested(void) {
124    GIVEN("nested paths under .ssh");
125    THEN("they are detected as sensitive");
126    TEST_ASSERT_TRUE_MESSAGE(filesystems::api::isSensitivePath("/.ssh/host_key"),
127      "device: /.ssh/host_key must be sensitive");
128    TEST_ASSERT_TRUE_MESSAGE(filesystems::api::isSensitivePath(".ssh/authorized_keys"),
129      "device: .ssh/authorized_keys must be sensitive");
130  }
131  
132  static void test_api_normal_path_not_sensitive(void) {
133    GIVEN("normal file paths");
134    THEN("they are not flagged as sensitive");
135    TEST_ASSERT_FALSE_MESSAGE(filesystems::api::isSensitivePath("/data.csv"),
136      "device: /data.csv must not be sensitive");
137    TEST_ASSERT_FALSE_MESSAGE(filesystems::api::isSensitivePath("/public/index.html"),
138      "device: /public/index.html must not be sensitive");
139    TEST_ASSERT_FALSE_MESSAGE(filesystems::api::isSensitivePath("data.csv"),
140      "device: data.csv must not be sensitive");
141  }
142  
143  void filesystems::api::test(void) {
144    MODULE("Filesystem API");
145    RUN_TEST(test_api_sensitive_path_with_slash);
146    RUN_TEST(test_api_sensitive_path_without_slash);
147    RUN_TEST(test_api_sensitive_path_nested);
148    RUN_TEST(test_api_normal_path_not_sensitive);
149  }
150  
151  #endif