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