api.ino 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. /*
  2. API.ino
  3. This script handle API requests
  4. functions.
  5. */
  6. /* Utilities Functions */
  7. String GetPara(AsyncWebServerRequest * request, String key) {
  8. if (request->hasParam(key)) {
  9. return request->getParam(key)->value();
  10. }
  11. return "";
  12. }
  13. void SendErrorResp(AsyncWebServerRequest * r, String errorMessage) {
  14. //Parse the error message into json
  15. StaticJsonDocument<200> jsonDocument;
  16. JsonObject root = jsonDocument.to<JsonObject>();
  17. root["error"] = errorMessage;
  18. String jsonString;
  19. serializeJson(root, jsonString);
  20. //Send it out with request handler
  21. r->send(200, "application/json", jsonString);
  22. }
  23. void SendJsonResp(AsyncWebServerRequest * r, String jsonString) {
  24. r->send(200, "application/json", jsonString);
  25. }
  26. void SendOK(AsyncWebServerRequest *r) {
  27. r->send(200, "application/json", "\"ok\"");
  28. }
  29. //Handle auth check if the request has been authed.
  30. //Return false if user is not authed
  31. bool HandleAuth(AsyncWebServerRequest *request) {
  32. //Handle for API calls authentication validate
  33. if (!IsUserAuthed(request)) {
  34. //user not logged in
  35. request->send(401, "text/html", "401 - Unauthorized");
  36. return false;
  37. }
  38. return true;
  39. }
  40. /*
  41. Handler Functions
  42. These are API endpoints handler
  43. which handle special API call
  44. for backend operations
  45. */
  46. /* Authentications */
  47. //Check if the user has logged in
  48. void HandleCheckAuth(AsyncWebServerRequest *r) {
  49. if (IsUserAuthed(r)) {
  50. SendJsonResp(r, "true");
  51. } else {
  52. SendJsonResp(r, "false");
  53. }
  54. }
  55. //Handle login request
  56. void HandleLogin(AsyncWebServerRequest *r) {
  57. String username = GetPara(r, "username");
  58. String password = GetPara(r, "password");
  59. if (adminUsername == "") {
  60. SendErrorResp(r, "admin account not enabled");
  61. return;
  62. }
  63. if (username.equals(adminUsername) && password.equals(adminPassword)) {
  64. //Username and password correct. Set a cookie for this login.
  65. //Generate a unique cookie for this login session
  66. String cookieId = GeneratedRandomHex();
  67. Serial.print("Generating new cookie ID ");
  68. Serial.println(cookieId);
  69. String expireUTC = getUTCTimeString(getTime() + 604800);
  70. Serial.print("Generating expire UTC timestamp ");
  71. Serial.println(expireUTC);
  72. AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
  73. response->addHeader("Server", mdnsName);
  74. response->addHeader("Cache-Control", "no-cache");
  75. response->addHeader("Set-Cookie", "web-auth=" + cookieId + "; Path=/; Expires=" + expireUTC + "; Max-Age=604800");
  76. //Save the cookie id
  77. DBWrite("auth", "cookie", cookieId);
  78. authSession = cookieId;
  79. //Return login succ
  80. r->send(response);
  81. Serial.println(username + " logged in");
  82. } else {
  83. SendErrorResp(r, "invalid username or password");
  84. return;
  85. }
  86. SendOK(r);
  87. }
  88. //Handle logout request, or you can logout with
  89. //just front-end by going to log:out@{ip_addr}/api/auth/logout
  90. void HandleLogout(AsyncWebServerRequest *r) {
  91. if (!IsUserAuthed(r)) {
  92. SendErrorResp(r, "not logged in");
  93. return;
  94. }
  95. //Remove the cookie on client side
  96. AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
  97. response->addHeader("Server", mdnsName);
  98. response->addHeader("Cache-Control", "no-cache");
  99. response->addHeader("Set-Cookie", "web-auth=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
  100. r->send(response);
  101. //Delete the server side cookie
  102. DBRemove("auth", "cookie");
  103. authSession = "";
  104. }
  105. /* File System Functions */
  106. //HandleListDir handle the listing of directory under /www/
  107. void HandleListDir(AsyncWebServerRequest *r) {
  108. if (!HandleAuth(r)) {
  109. return;
  110. }
  111. //Get the folder path to be listed
  112. //As ESP8266 dont have enough memory for proper struct to json conv, we are hacking a json string out of a single for-loop
  113. String jsonString = "[";
  114. String folderPath = GetPara(r, "dir");
  115. folderPath = "/www" + folderPath;
  116. if (SD.exists(folderPath)) {
  117. File root = SD.open(folderPath);
  118. bool firstObject = true;
  119. if (root) {
  120. while (true) {
  121. File entry = root.openNextFile();
  122. if (!entry) {
  123. // No more files
  124. break;
  125. } else {
  126. //There are more lines. Add a , to the end of the previous json object
  127. if (!firstObject) {
  128. jsonString = jsonString + ",";
  129. } else {
  130. firstObject = false;
  131. }
  132. }
  133. String isDirString = "true";
  134. if (!entry.isDirectory()) {
  135. isDirString = "false";
  136. }
  137. jsonString = jsonString + "{\"Filename\":\"" + entry.name() + "\",\"Filesize\":" + String(entry.size()) + ",\"IsDir\":" + isDirString + "}";
  138. entry.close();
  139. }
  140. root.close();
  141. jsonString += "]";
  142. SendJsonResp(r, jsonString);
  143. } else {
  144. SendErrorResp(r, "path read failed");
  145. }
  146. } else {
  147. SendErrorResp(r, "path not found");
  148. }
  149. Serial.println(folderPath);
  150. }
  151. //Handle file rename on SD card
  152. void HandleFileRename(AsyncWebServerRequest *r) {
  153. if (!HandleAuth(r)) {
  154. return;
  155. }
  156. String srcFile = GetPara(r, "src");
  157. String destFile = GetPara(r, "dest");
  158. srcFile.trim();
  159. destFile.trim();
  160. srcFile = "/www" + srcFile;
  161. destFile = "/www" + destFile;
  162. if (!SD.exists(srcFile)) {
  163. SendErrorResp(r, "source file not exists");
  164. return;
  165. }
  166. if (SD.exists(destFile)) {
  167. SendErrorResp(r, "destination file already exists");
  168. return;
  169. }
  170. SD.rename(srcFile, destFile);
  171. SendOK(r);
  172. }
  173. //Handle file delete on SD card
  174. void HandleFileDel(AsyncWebServerRequest *r) {
  175. if (!HandleAuth(r)) {
  176. return;
  177. }
  178. String targetFile = GetPara(r, "target");
  179. targetFile.trim();
  180. if (targetFile.equals("/")) {
  181. //Do not allow remove the whole root folder
  182. SendErrorResp(r, "you cannot remove the root folder");
  183. return;
  184. }
  185. targetFile = "/www" + targetFile;
  186. if (!SD.exists(targetFile)) {
  187. SendErrorResp(r, "target file not exists");
  188. return;
  189. }
  190. if (!SD.remove(targetFile)) {
  191. if (IsDir(targetFile)) {
  192. if (!targetFile.endsWith("/")) {
  193. //pad the tailing slash
  194. targetFile = targetFile + "/";
  195. }
  196. //Try remove dir
  197. if (!SD.rmdir(targetFile)) {
  198. //The folder might contain stuffs. Do recursive delete
  199. Serial.println("rmdir failed. Trying recursive delete");
  200. if (recursiveDirRemove(targetFile)) {
  201. SendOK(r);
  202. } else {
  203. SendErrorResp(r, "folder delete failed");
  204. }
  205. } else {
  206. SendOK(r);
  207. }
  208. } else {
  209. SendErrorResp(r, "file delete failed");
  210. }
  211. return;
  212. }
  213. SendOK(r);
  214. }
  215. //Hanle creating new Folder
  216. void HandleNewFolder(AsyncWebServerRequest *r) {
  217. if (!HandleAuth(r)) {
  218. return;
  219. }
  220. //Get and clean the folder path
  221. String folderPath = GetPara(r, "path");
  222. folderPath.trim();
  223. if (folderPath == "") {
  224. SendErrorResp(r, "invalid folder path given");
  225. return;
  226. }
  227. folderPath = "/www" + folderPath;
  228. if (SD.exists(folderPath)) {
  229. //Already exists
  230. SendErrorResp(r, "folder with given path already exists");
  231. return;
  232. }
  233. SD.mkdir(folderPath);
  234. SendOK(r);
  235. }
  236. //Handle download file from any path, including the private store folder
  237. void HandleFileDownload(AsyncWebServerRequest *r) {
  238. if (!HandleAuth(r)) {
  239. return;
  240. }
  241. String targetFile = GetPara(r, "file");
  242. String preview = GetPara(r, "preview");
  243. if (!SD.exists("/www" + targetFile)) {
  244. r->send(404, "text/html", "404 - File Not Found");
  245. return;
  246. }
  247. //Check if it is dir, ESP8266 have no power of creating zip file
  248. if (IsDir("/www" + targetFile)) {
  249. r->send(500, "text/html", "500 - Internal Server Error: Target is a folder");
  250. return;
  251. }
  252. //Ok. Proceed with file serving
  253. if (preview == "true") {
  254. //Serve
  255. r->send(SDFS, "/www" + targetFile, getMime(targetFile), false);
  256. } else {
  257. //Download
  258. r->send(SDFS, "/www" + targetFile, "application/octet-stream", false);
  259. }
  260. }
  261. //Get the file / folder properties
  262. void HandleFileProp(AsyncWebServerRequest *r) {
  263. if (!HandleAuth(r)) {
  264. return;
  265. }
  266. String filepath = GetPara(r, "file");
  267. filepath.trim();
  268. String realFilepath = "/www" + filepath;
  269. String resp = "";
  270. if (IsDir(realFilepath)) {
  271. //target is a folder
  272. uint32_t totalSize = 0;
  273. uint16_t fileCount = 0;
  274. uint16_t folderCount = 0;
  275. analyzeDirectory(realFilepath, totalSize, fileCount, folderCount);
  276. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":true,\"filesize\":" + String(totalSize) + ",\"fileCounts\":" + String(fileCount) + ",\"folderCounts\":" + String(folderCount) + "}";
  277. } else {
  278. //target is a file
  279. File targetFile = SD.open(realFilepath);
  280. if (!targetFile) {
  281. SendErrorResp(r, "File open failed");
  282. return;
  283. }
  284. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":false,\"filesize\":" + String(targetFile.size()) + "}";
  285. targetFile.close();
  286. }
  287. SendJsonResp(r, resp);
  288. }
  289. //Search with basic keyword match
  290. void HandleFileSearch(AsyncWebServerRequest *r) {
  291. if (!HandleAuth(r)) {
  292. return;
  293. }
  294. String keyword = GetPara(r, "keyword");
  295. keyword.trim();
  296. if (keyword.equals("")) {
  297. SendErrorResp(r, "keyword cannot be empty");
  298. return;
  299. }
  300. //Prepare for the resp
  301. AsyncResponseStream *response = r->beginResponseStream("application/json");
  302. response->print("[");
  303. //Recursive search for the whole /www/ directory (Require tailing slash)
  304. int foundCounter = 0;
  305. scanSDCardForKeyword("/www/", keyword, &foundCounter, response);
  306. //Send the resp
  307. response->print("]");
  308. r->send(response);
  309. }
  310. /* Handle Load and Set of Prefernece Value */
  311. //Set Preference, auth user only
  312. void HandleSetPref(AsyncWebServerRequest *r) {
  313. if (!HandleAuth(r)) {
  314. return;
  315. }
  316. String key = GetPara(r, "key");
  317. String val = GetPara(r, "value");
  318. key.trim();
  319. val.trim();
  320. if (key == "" || val == "") {
  321. SendErrorResp(r, "invalid key or value given");
  322. return;
  323. }
  324. DBWrite("pref", key, val);
  325. SendOK(r);
  326. }
  327. //Load Prefernece, allow public access
  328. void HandleLoadPref(AsyncWebServerRequest *r) {
  329. String key = GetPara(r, "key");
  330. key.trim();
  331. if (!DBKeyExists("pref", key)) {
  332. SendErrorResp(r, "preference with given key not found");
  333. return;
  334. }
  335. String prefValue = DBRead("pref", key);
  336. r->send(200, "application/json", "\"" + prefValue + "\"");
  337. }
  338. /* Handle System Info */
  339. void HandleLoadSpaceInfo(AsyncWebServerRequest *r) {
  340. String jsonResp = "{\
  341. \"diskSpace\":" + String(getSDCardTotalSpace()) + ",\
  342. \"usedSpace\": " + String(getSDCardUsedSpace()) + "\
  343. }";
  344. SendJsonResp(r, jsonResp);
  345. }
  346. //Get the current connected WiFi info
  347. void HandleWiFiInfo(AsyncWebServerRequest *r) {
  348. StaticJsonDocument<256> jsonBuffer;
  349. jsonBuffer["SSID"] = WiFi.SSID();
  350. jsonBuffer["WifiStatus"] = WiFi.status();
  351. jsonBuffer["WifiStrength"] = WiFi.RSSI();
  352. jsonBuffer["MAC"] = WiFi.macAddress();
  353. jsonBuffer["IP"] = WiFi.localIP().toString();
  354. jsonBuffer["Subnet"] = WiFi.subnetMask().toString();
  355. jsonBuffer["Gateway"] = WiFi.gatewayIP().toString();
  356. jsonBuffer["DNS1"] = WiFi.dnsIP(0).toString();
  357. jsonBuffer["DNS2"] = WiFi.dnsIP(1).toString();
  358. jsonBuffer["DNS3"] = WiFi.dnsIP(2).toString();
  359. // Serialize the JSON buffer to a string
  360. String jsonString;
  361. serializeJson(jsonBuffer, jsonString);
  362. SendJsonResp(r, jsonString);
  363. }