api.ino 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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 folderPath = GetPara(r, "dir");
  145. folderPath = "/www" + folderPath;
  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. jsonString = jsonString + "{\"Filename\":\"" + entry.name() + "\",\"Filesize\":" + String(entry.size()) + ",\"IsDir\":" + isDirString + "}";
  168. entry.close();
  169. }
  170. root.close();
  171. jsonString += "]";
  172. SendJsonResp(r, jsonString);
  173. } else {
  174. SendErrorResp(r, "path read failed");
  175. }
  176. } else {
  177. SendErrorResp(r, "path not found");
  178. }
  179. Serial.println(folderPath);
  180. }
  181. //Handle file rename on SD card
  182. void HandleFileRename(AsyncWebServerRequest *r) {
  183. if (!HandleAuth(r)) {
  184. return;
  185. }
  186. String srcFile = GetPara(r, "src");
  187. String destFile = GetPara(r, "dest");
  188. srcFile.trim();
  189. destFile.trim();
  190. srcFile = "/www" + srcFile;
  191. destFile = "/www" + destFile;
  192. if (!SD.exists(srcFile)) {
  193. SendErrorResp(r, "source file not exists");
  194. return;
  195. }
  196. if (SD.exists(destFile)) {
  197. SendErrorResp(r, "destination file already exists");
  198. return;
  199. }
  200. SD.rename(srcFile, destFile);
  201. SendOK(r);
  202. }
  203. //Handle file delete on SD card
  204. void HandleFileDel(AsyncWebServerRequest *r) {
  205. if (!HandleAuth(r)) {
  206. return;
  207. }
  208. String targetFile = GetPara(r, "target");
  209. targetFile.trim();
  210. if (targetFile.equals("/")) {
  211. //Do not allow remove the whole root folder
  212. SendErrorResp(r, "you cannot remove the root folder");
  213. return;
  214. }
  215. targetFile = "/www" + targetFile;
  216. if (!SD.exists(targetFile)) {
  217. SendErrorResp(r, "target file not exists");
  218. return;
  219. }
  220. if (!SD.remove(targetFile)) {
  221. if (IsDir(targetFile)) {
  222. if (!targetFile.endsWith("/")) {
  223. //pad the tailing slash
  224. targetFile = targetFile + "/";
  225. }
  226. //Try remove dir
  227. if (!SD.rmdir(targetFile)) {
  228. //The folder might contain stuffs. Do recursive delete
  229. Serial.println("rmdir failed. Trying recursive delete");
  230. if (recursiveDirRemove(targetFile)) {
  231. SendOK(r);
  232. } else {
  233. SendErrorResp(r, "folder delete failed");
  234. }
  235. } else {
  236. SendOK(r);
  237. }
  238. } else {
  239. SendErrorResp(r, "file delete failed");
  240. }
  241. return;
  242. }
  243. SendOK(r);
  244. }
  245. //Hanle creating new Folder
  246. void HandleNewFolder(AsyncWebServerRequest *r) {
  247. if (!HandleAuth(r)) {
  248. return;
  249. }
  250. //Get and clean the folder path
  251. String folderPath = GetPara(r, "path");
  252. folderPath.trim();
  253. if (folderPath == "") {
  254. SendErrorResp(r, "invalid folder path given");
  255. return;
  256. }
  257. folderPath = "/www" + folderPath;
  258. if (SD.exists(folderPath)) {
  259. //Already exists
  260. SendErrorResp(r, "folder with given path already exists");
  261. return;
  262. }
  263. SD.mkdir(folderPath);
  264. SendOK(r);
  265. }
  266. //Handle download file from any path, including the private store folder
  267. void HandleFileDownload(AsyncWebServerRequest *r) {
  268. if (!HandleAuth(r)) {
  269. return;
  270. }
  271. String targetFile = GetPara(r, "file");
  272. String preview = GetPara(r, "preview");
  273. if (!SD.exists("/www" + targetFile)) {
  274. r->send(404, "text/html", "404 - File Not Found");
  275. return;
  276. }
  277. //Check if it is dir, ESP8266 have no power of creating zip file
  278. if (IsDir("/www" + targetFile)) {
  279. r->send(500, "text/html", "500 - Internal Server Error: Target is a folder");
  280. return;
  281. }
  282. //Ok. Proceed with file serving
  283. if (preview == "true") {
  284. //Serve
  285. r->send(SDFS, "/www" + targetFile, getMime(targetFile), false);
  286. } else {
  287. //Download
  288. r->send(SDFS, "/www" + targetFile, "application/octet-stream", false);
  289. }
  290. }
  291. //Get the file / folder properties
  292. void HandleFileProp(AsyncWebServerRequest *r) {
  293. if (!HandleAuth(r)) {
  294. return;
  295. }
  296. String filepath = GetPara(r, "file");
  297. filepath.trim();
  298. String realFilepath = "/www" + filepath;
  299. String resp = "";
  300. if (IsDir(realFilepath)) {
  301. //target is a folder
  302. uint32_t totalSize = 0;
  303. uint16_t fileCount = 0;
  304. uint16_t folderCount = 0;
  305. analyzeDirectory(realFilepath, totalSize, fileCount, folderCount);
  306. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":true,\"filesize\":" + String(totalSize) + ",\"fileCounts\":" + String(fileCount) + ",\"folderCounts\":" + String(folderCount) + "}";
  307. } else {
  308. //target is a file
  309. File targetFile = SD.open(realFilepath);
  310. if (!targetFile) {
  311. SendErrorResp(r, "File open failed");
  312. return;
  313. }
  314. resp = "{\"filename\":\"" + basename(filepath) + "\",\"filepath\":\"" + filepath + "\",\"isDir\":false,\"filesize\":" + String(targetFile.size()) + "}";
  315. targetFile.close();
  316. }
  317. SendJsonResp(r, resp);
  318. }
  319. //Search with basic keyword match
  320. void HandleFileSearch(AsyncWebServerRequest *r) {
  321. if (!HandleAuth(r)) {
  322. return;
  323. }
  324. String keyword = GetPara(r, "keyword");
  325. keyword.trim();
  326. if (keyword.equals("")) {
  327. SendErrorResp(r, "keyword cannot be empty");
  328. return;
  329. }
  330. //Prepare for the resp
  331. AsyncResponseStream *response = r->beginResponseStream("application/json");
  332. response->print("[");
  333. //Recursive search for the whole /www/ directory (Require tailing slash)
  334. int foundCounter = 0;
  335. scanSDCardForKeyword("/www/", keyword, &foundCounter, response);
  336. //Send the resp
  337. response->print("]");
  338. r->send(response);
  339. }
  340. /* Handle Load and Set of Prefernece Value */
  341. //Set Preference, auth user only
  342. void HandleSetPref(AsyncWebServerRequest *r) {
  343. if (!HandleAuth(r)) {
  344. return;
  345. }
  346. String key = GetPara(r, "key");
  347. String val = GetPara(r, "value");
  348. key.trim();
  349. val.trim();
  350. if (key == "" || val == "") {
  351. SendErrorResp(r, "invalid key or value given");
  352. return;
  353. }
  354. DBWrite("pref", key, val);
  355. SendOK(r);
  356. }
  357. //Load Prefernece, allow public access
  358. void HandleLoadPref(AsyncWebServerRequest *r) {
  359. String key = GetPara(r, "key");
  360. key.trim();
  361. if (!DBKeyExists("pref", key)) {
  362. SendErrorResp(r, "preference with given key not found");
  363. return;
  364. }
  365. String prefValue = DBRead("pref", key);
  366. r->send(200, "application/json", "\"" + prefValue + "\"");
  367. }
  368. /* Handle System Info */
  369. void HandleLoadSpaceInfo(AsyncWebServerRequest *r) {
  370. String jsonResp = "{\
  371. \"diskSpace\":" + String(getSDCardTotalSpace())
  372. + ",\
  373. \"usedSpace\": " + String(getSDCardUsedSpace())
  374. + "\
  375. }";
  376. SendJsonResp(r, jsonResp);
  377. }
  378. /* Handle Wake On Lan Request */
  379. void HandleWakeOnLan(AsyncWebServerRequest *r) {
  380. if (!IsUserAuthed(r)) {
  381. SendErrorResp(r, "not logged in");
  382. return;
  383. }
  384. if (r->hasArg("mac")) {
  385. String macAddress = r->arg("mac");
  386. Serial.print("Sending WoL packet to: ");
  387. Serial.println(macAddress);
  388. //Send MAC address to both port 9 and 7
  389. WOL.sendMagicPacket(macAddress);
  390. WOL.sendMagicPacket(macAddress, 7);
  391. r->send(200, "text/plain", "Received MAC Address: " + macAddress);
  392. return;
  393. } else {
  394. r->send(400, "text/plain", "Missing 'mac' parameter");
  395. return;
  396. }
  397. }
  398. //Get the current connected WiFi info
  399. void HandleWiFiInfo(AsyncWebServerRequest *r) {
  400. StaticJsonDocument<256> jsonBuffer;
  401. jsonBuffer["SSID"] = WiFi.SSID();
  402. jsonBuffer["WifiStatus"] = WiFi.status();
  403. jsonBuffer["WifiStrength"] = WiFi.RSSI();
  404. jsonBuffer["MAC"] = WiFi.macAddress();
  405. jsonBuffer["IP"] = WiFi.localIP().toString();
  406. jsonBuffer["Subnet"] = WiFi.subnetMask().toString();
  407. jsonBuffer["Gateway"] = WiFi.gatewayIP().toString();
  408. jsonBuffer["DNS1"] = WiFi.dnsIP(0).toString();
  409. jsonBuffer["DNS2"] = WiFi.dnsIP(1).toString();
  410. jsonBuffer["DNS3"] = WiFi.dnsIP(2).toString();
  411. // Serialize the JSON buffer to a string
  412. String jsonString;
  413. serializeJson(jsonBuffer, jsonString);
  414. SendJsonResp(r, jsonString);
  415. }