api.ino 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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. return;
  83. } else if (UserCheckAuth(username, password)) {
  84. //User Login. Generate a session for this user
  85. String cookieId = GeneratedRandomHex();
  86. Serial.print("Generating new cookie ID ");
  87. Serial.println(cookieId);
  88. String expireUTC = getUTCTimeString(getTime() + 604800);
  89. Serial.print("Generating expire UTC timestamp ");
  90. Serial.println(expireUTC);
  91. AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
  92. response->addHeader("Server", mdnsName);
  93. response->addHeader("Cache-Control", "no-cache");
  94. response->addHeader("Set-Cookie", "web-auth=" + cookieId + "; Path=/; Expires=" + expireUTC + "; Max-Age=604800");
  95. //Save the cookie id
  96. DBWrite("sess", cookieId, username);
  97. //Return login succ
  98. r->send(response);
  99. Serial.println(username + " logged in");
  100. return;
  101. } else {
  102. SendErrorResp(r, "invalid username or password");
  103. return;
  104. }
  105. SendOK(r);
  106. }
  107. //Handle logout request, or you can logout with
  108. //just front-end by going to log:out@{ip_addr}/api/auth/logout
  109. void HandleLogout(AsyncWebServerRequest *r) {
  110. if (!IsUserAuthed(r)) {
  111. SendErrorResp(r, "not logged in");
  112. return;
  113. }
  114. //Delete the server side cookie
  115. if (IsAdmin(r)) {
  116. DBRemove("auth", "cookie");
  117. authSession = "";
  118. } else {
  119. //Get the session from user
  120. String authCookie = GetCookieValueByKey(r, "web-auth");
  121. if (authCookie == "") {
  122. SendErrorResp(r, "unknown error: unable to read cookie from header");
  123. return;
  124. }
  125. //Remove the session map
  126. DBRemove("sess", authCookie);
  127. }
  128. //Remove the cookie on client side
  129. AsyncWebServerResponse *response = r->beginResponse(200, "application/json", "\"ok\"");
  130. response->addHeader("Server", mdnsName);
  131. response->addHeader("Cache-Control", "no-cache");
  132. response->addHeader("Set-Cookie", "web-auth=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
  133. r->send(response);
  134. }
  135. /* File System Functions */
  136. //HandleListDir handle the listing of directory under /www/
  137. void HandleListDir(AsyncWebServerRequest *r) {
  138. if (!HandleAuth(r)) {
  139. return;
  140. }
  141. //Get the folder path to be listed
  142. //As ESP8266 dont have enough memory for proper struct to json conv, we are hacking a json string out of a single for-loop
  143. String jsonString = "[";
  144. String folderSubPath = GetPara(r, "dir");
  145. String folderPath = "/www" + folderSubPath;
  146. if (SD.exists(folderPath)) {
  147. File root = SD.open(folderPath);
  148. bool firstObject = true;
  149. if (root) {
  150. while (true) {
  151. File entry = root.openNextFile();
  152. if (!entry) {
  153. // No more files
  154. break;
  155. } else {
  156. //There are more lines. Add a , to the end of the previous json object
  157. if (!firstObject) {
  158. jsonString = jsonString + ",";
  159. } else {
  160. firstObject = false;
  161. }
  162. }
  163. String isDirString = "true";
  164. if (!entry.isDirectory()) {
  165. isDirString = "false";
  166. }
  167. //Get share UUID, return empty string if not shared
  168. //prefix "/www" have to be removed for share entry lookup
  169. String shareUUID = GetFileShareIDByFilename(folderSubPath + entry.name());
  170. jsonString = jsonString + "{\"Filename\":\"" + entry.name() + "\",\"Filesize\":" + String(entry.size()) + ",\"IsDir\":" + isDirString + ",\"Share\":\"" + shareUUID + "\"}";
  171. entry.close();
  172. }
  173. root.close();
  174. jsonString += "]";
  175. SendJsonResp(r, jsonString);
  176. } else {
  177. SendErrorResp(r, "path read failed");
  178. }
  179. } else {
  180. SendErrorResp(r, "path not found");
  181. }
  182. Serial.println(folderPath);
  183. }
  184. //Handle file rename on SD card
  185. void HandleFileRename(AsyncWebServerRequest *r) {
  186. if (!HandleAuth(r)) {
  187. return;
  188. }
  189. String srcFile = GetPara(r, "src");
  190. String destFile = GetPara(r, "dest");
  191. srcFile.trim();
  192. destFile.trim();
  193. srcFile = "/www" + srcFile;
  194. destFile = "/www" + destFile;
  195. if (!SD.exists(srcFile)) {
  196. SendErrorResp(r, "source file not exists");
  197. return;
  198. }
  199. if (SD.exists(destFile)) {
  200. SendErrorResp(r, "destination file already exists");
  201. return;
  202. }
  203. SD.rename(srcFile, destFile);
  204. SendOK(r);
  205. }
  206. //Handle file delete on SD card
  207. void HandleFileDel(AsyncWebServerRequest *r) {
  208. if (!HandleAuth(r)) {
  209. return;
  210. }
  211. String targetFile = GetPara(r, "target");
  212. targetFile.trim();
  213. if (targetFile.equals("/")) {
  214. //Do not allow remove the whole root folder
  215. SendErrorResp(r, "you cannot remove the root folder");
  216. return;
  217. }
  218. targetFile = "/www" + targetFile;
  219. if (!SD.exists(targetFile)) {
  220. SendErrorResp(r, "target file not exists");
  221. return;
  222. }
  223. if (!SD.remove(targetFile)) {
  224. if (IsDir(targetFile)) {
  225. if (!targetFile.endsWith("/")) {
  226. //pad the tailing slash
  227. targetFile = targetFile + "/";
  228. }
  229. //Try remove dir
  230. if (!SD.rmdir(targetFile)) {
  231. //The folder might contain stuffs. Do recursive delete
  232. Serial.println("rmdir failed. Trying recursive delete");
  233. if (recursiveDirRemove(targetFile)) {
  234. SendOK(r);
  235. } else {
  236. SendErrorResp(r, "folder delete failed");
  237. }
  238. } else {
  239. SendOK(r);
  240. }
  241. } else {
  242. SendErrorResp(r, "file delete failed");
  243. }
  244. return;
  245. }
  246. SendOK(r);
  247. }
  248. //Hanle creating new Folder
  249. void HandleNewFolder(AsyncWebServerRequest *r) {
  250. if (!HandleAuth(r)) {
  251. return;
  252. }
  253. //Get and clean the folder path
  254. String folderPath = GetPara(r, "path");
  255. folderPath.trim();
  256. if (folderPath == "") {
  257. SendErrorResp(r, "invalid folder path given");
  258. return;
  259. }
  260. folderPath = "/www" + folderPath;
  261. if (SD.exists(folderPath)) {
  262. //Already exists
  263. SendErrorResp(r, "folder with given path already exists");
  264. return;
  265. }
  266. SD.mkdir(folderPath);
  267. SendOK(r);
  268. }
  269. //Handle download file from any path, including the private store folder
  270. void HandleFileDownload(AsyncWebServerRequest *r) {
  271. if (!HandleAuth(r)) {
  272. return;
  273. }
  274. String targetFile = GetPara(r, "file");
  275. String preview = GetPara(r, "preview");
  276. if (!SD.exists("/www" + targetFile)) {
  277. r->send(404, "text/html", "404 - File Not Found");
  278. return;
  279. }
  280. //Check if it is dir, ESP8266 have no power of creating zip file
  281. if (IsDir("/www" + targetFile)) {
  282. r->send(500, "text/html", "500 - Internal Server Error: Target is a folder");
  283. return;
  284. }
  285. //Ok. Proceed with file serving
  286. if (preview == "true") {
  287. //Serve
  288. r->send(SDFS, "/www" + targetFile, getMime(targetFile), false);
  289. } else {
  290. //Download
  291. r->send(SDFS, "/www" + targetFile, "application/octet-stream", false);
  292. }
  293. }
  294. //Get the file / folder properties
  295. void HandleFileProp(AsyncWebServerRequest *r) {
  296. if (!HandleAuth(r)) {
  297. return;
  298. }
  299. String filepath = GetPara(r, "file");
  300. filepath.trim();
  301. String realFilepath = "/www" + filepath;
  302. String resp = "";
  303. if (IsDir(realFilepath)) {
  304. //target is a folder
  305. uint32_t totalSize = 0;
  306. uint16_t fileCount = 0;
  307. uint16_t folderCount = 0;
  308. analyzeDirectory(realFilepath, totalSize, fileCount, folderCount);
  309. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":true,\"filesize\":" + String(totalSize) + ",\"fileCounts\":" + String(fileCount) + ",\"folderCounts\":" + String(folderCount) + "}";
  310. } else {
  311. //target is a file
  312. File targetFile = SD.open(realFilepath);
  313. if (!targetFile) {
  314. SendErrorResp(r, "File open failed");
  315. return;
  316. }
  317. String shareID = GetFileShareIDByFilename(filepath);
  318. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":false,\"filesize\":" + String(targetFile.size()) + ",\"shareid\":\"" + shareID + "\"}";
  319. targetFile.close();
  320. }
  321. SendJsonResp(r, resp);
  322. }
  323. //Search with basic keyword match
  324. void HandleFileSearch(AsyncWebServerRequest *r) {
  325. if (!HandleAuth(r)) {
  326. return;
  327. }
  328. String keyword = GetPara(r, "keyword");
  329. keyword.trim();
  330. if (keyword.equals("")) {
  331. SendErrorResp(r, "keyword cannot be empty");
  332. return;
  333. }
  334. //Prepare for the resp
  335. AsyncResponseStream *response = r->beginResponseStream("application/json");
  336. response->print("[");
  337. //Recursive search for the whole /www/ directory (Require tailing slash)
  338. int foundCounter = 0;
  339. scanSDCardForKeyword("/www/", keyword, &foundCounter, response);
  340. //Send the resp
  341. response->print("]");
  342. r->send(response);
  343. }
  344. /* Handle Load and Set of Prefernece Value */
  345. //Set Preference, auth user only
  346. void HandleSetPref(AsyncWebServerRequest *r) {
  347. if (!HandleAuth(r)) {
  348. return;
  349. }
  350. String key = GetPara(r, "key");
  351. String val = GetPara(r, "value");
  352. key.trim();
  353. val.trim();
  354. if (key == "" || val == "") {
  355. SendErrorResp(r, "invalid key or value given");
  356. return;
  357. }
  358. DBWrite("pref", key, val);
  359. SendOK(r);
  360. }
  361. //Load Prefernece, allow public access
  362. void HandleLoadPref(AsyncWebServerRequest *r) {
  363. String key = GetPara(r, "key");
  364. key.trim();
  365. if (!DBKeyExists("pref", key)) {
  366. SendErrorResp(r, "preference with given key not found");
  367. return;
  368. }
  369. String prefValue = DBRead("pref", key);
  370. r->send(200, "application/json", "\"" + prefValue + "\"");
  371. }
  372. /* Handle System Info */
  373. void HandleLoadSpaceInfo(AsyncWebServerRequest *r) {
  374. String jsonResp = "{\
  375. \"diskSpace\":" + String(getSDCardTotalSpace())
  376. + ",\
  377. \"usedSpace\": " + String(getSDCardUsedSpace())
  378. + "\
  379. }";
  380. SendJsonResp(r, jsonResp);
  381. }
  382. /* Handle Wake On Lan Request */
  383. void HandleWakeOnLan(AsyncWebServerRequest *r) {
  384. if (!IsUserAuthed(r)) {
  385. SendErrorResp(r, "not logged in");
  386. return;
  387. }
  388. if (r->hasArg("mac")) {
  389. String macAddress = r->arg("mac");
  390. Serial.print("Sending WoL packet to: ");
  391. Serial.println(macAddress);
  392. //Send MAC address to both port 9 and 7
  393. WOL.sendMagicPacket(macAddress);
  394. WOL.sendMagicPacket(macAddress, 7);
  395. r->send(200, "text/plain", "Received MAC Address: " + macAddress);
  396. return;
  397. } else {
  398. r->send(400, "text/plain", "Missing 'mac' parameter");
  399. return;
  400. }
  401. }
  402. //Get the current connected WiFi info
  403. void HandleWiFiInfo(AsyncWebServerRequest *r) {
  404. StaticJsonDocument<256> jsonBuffer;
  405. jsonBuffer["SSID"] = WiFi.SSID();
  406. jsonBuffer["WifiStatus"] = WiFi.status();
  407. jsonBuffer["WifiStrength"] = WiFi.RSSI();
  408. jsonBuffer["MAC"] = WiFi.macAddress();
  409. jsonBuffer["IP"] = WiFi.localIP().toString();
  410. jsonBuffer["Subnet"] = WiFi.subnetMask().toString();
  411. jsonBuffer["Gateway"] = WiFi.gatewayIP().toString();
  412. jsonBuffer["DNS1"] = WiFi.dnsIP(0).toString();
  413. jsonBuffer["DNS2"] = WiFi.dnsIP(1).toString();
  414. jsonBuffer["DNS3"] = WiFi.dnsIP(2).toString();
  415. // Serialize the JSON buffer to a string
  416. String jsonString;
  417. serializeJson(jsonBuffer, jsonString);
  418. SendJsonResp(r, jsonString);
  419. }