├── .gitignore ├── LICENSE ├── README.md ├── firmware ├── v2 │ └── web-server │ │ ├── api.ino │ │ ├── internal.ino │ │ ├── kvdb.ino │ │ ├── router.ino │ │ ├── server.ino │ │ ├── upload.ino │ │ ├── utils.ino │ │ └── web-server.ino ├── v3 │ └── web-server │ │ ├── api.ino │ │ ├── internal.ino │ │ ├── kvdb.ino │ │ ├── lib │ │ └── WakeOnLan.zip │ │ ├── router.ino │ │ ├── server.ino │ │ ├── share.ino │ │ ├── upload.ino │ │ ├── users.ino │ │ ├── utils.ino │ │ ├── web-server.ino │ │ ├── web-server.ino.d1_mini.bin │ │ └── web-server.ino.d1_mini.zip └── v4 │ └── web-server │ ├── api.ino │ ├── internal.ino │ ├── kvdb.ino │ ├── lib │ └── WakeOnLan.zip │ ├── router.ino │ ├── server.ino │ ├── share.ino │ ├── upload.ino │ ├── users.ino │ ├── utils.ino │ ├── web-server.ino │ ├── web-server.ino.d1_mini.bin │ └── web-server.ino.d1_mini.zip ├── img └── README │ ├── 1.jpg │ ├── image-20230814160329296.png │ ├── image-20230814160331808.png │ ├── image-20230814160505939.png │ ├── image-20230814160534701.png │ ├── image-20230814160904725.png │ ├── image-20230814161008552.png │ ├── image-20230814161027242.png │ ├── image-20230814161040075.png │ ├── image-20230814161104181.png │ ├── image-20240323120647781.png │ ├── image-20240323120841577.png │ ├── image-20240323120851503.png │ ├── image-20240323120859631.png │ ├── image-20240323120923170.png │ ├── image-20240323120934323.png │ ├── image-20240323121044080.png │ ├── image-20240323122057879.png │ ├── image-20250510203307491.png │ ├── image-20250510203347375.png │ ├── image-20250510203411915.png │ ├── image-20250510203428665.png │ ├── image-20250510203529903.png │ ├── image-20250510203605617.png │ ├── image-20250510203636902.png │ └── image-20250510203736747.png ├── pcb ├── v2 │ ├── BOM_webstick v2_2023-08-18.csv │ ├── Gerber_webstick v2.1.zip │ ├── Gerber_webstick v2.zip │ ├── PCB_webstick v2_2023-11-09.json │ └── PickAndPlace_webstick v2_2023-08-18.csv ├── v3 │ ├── BOM_Website-Stick_2024-03-20.csv │ ├── Gerber_Website-Stick_webstick-v3_2024-03-20.zip │ ├── PCB_webstick-v3_2024-03-20.json │ └── PickAndPlace_webstick-v3_2024-03-20.csv └── v4 │ ├── BOM_Website-Stick_2025-02-28.csv │ ├── Gerber_Website-Stick_webstick-v4_2025-02-28.zip │ ├── PCB_webstick-v4_2025-02-28.json │ └── PickAndPlace_webstick-v4_2025-02-28.csv ├── res ├── favicon.png ├── favicon.psd ├── logo.png └── logo.psd └── sd_card ├── README.md ├── cfg ├── admin.txt ├── mdns.txt └── wifi.txt └── www ├── about.html ├── admin ├── fs.css ├── fs.html ├── img │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── cluster.svg │ ├── eq.svg │ ├── file.svg │ ├── folder.svg │ ├── icon.svg │ ├── network.svg │ ├── opr │ │ ├── copy.svg │ │ ├── delete.svg │ │ ├── download.svg │ │ ├── new_folder.svg │ │ ├── open.svg │ │ ├── paste.svg │ │ ├── share.svg │ │ └── upload.svg │ ├── post-engine.svg │ ├── user.svg │ ├── users.svg │ ├── wifi.svg │ ├── zoom-in.svg │ └── zoom-out.svg ├── index.html ├── info.html ├── mde │ ├── index.html │ └── script │ │ ├── mde.css │ │ └── mde.js ├── music.html ├── notepad │ ├── ace │ │ └── editor.html │ └── index.html ├── photo.html ├── post.html ├── posteng │ ├── all.html │ ├── edit.html │ ├── library.html │ ├── new.html │ ├── newpg.html │ ├── pages.html │ ├── paste.html │ ├── pedit.html │ ├── setting.html │ └── src │ │ └── en.js ├── script │ ├── DPlayer.min.js │ └── DPlayer.min.js.map ├── search.html ├── share.html ├── shares.html ├── upld.js ├── users.html └── video.html ├── down ├── Rise Above.mp3 ├── minecraft.webm ├── photo.jpg └── text.txt ├── favicon.png ├── img ├── logo.png ├── selfie.jpg └── wallpaper.jpg ├── index.html ├── login.html ├── posts.html ├── qr.html ├── site ├── img │ └── index.html ├── index.html ├── pages │ └── index.html └── posts │ ├── 1710676940_Hello World!.md │ └── index.html └── store └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> C 2 | # Object files 3 | *.o 4 | *.ko 5 | *.obj 6 | *.elf 7 | 8 | # Precompiled Headers 9 | *.gch 10 | *.pch 11 | 12 | # Libraries 13 | *.lib 14 | *.a 15 | *.la 16 | *.lo 17 | 18 | # Shared objects (inc. Windows DLLs) 19 | *.dll 20 | *.so 21 | *.so.* 22 | *.dylib 23 | 24 | # Executables 25 | *.exe 26 | *.out 27 | *.app 28 | *.i*86 29 | *.x86_64 30 | *.hex 31 | 32 | # Debug files 33 | *.dSYM/ 34 | 35 | autopush.sh 36 | 37 | -------------------------------------------------------------------------------- /firmware/v2/web-server/kvdb.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Key Value database 4 | 5 | This is a file system based database 6 | that uses foldername as table name, 7 | filename as key and content as value 8 | 9 | Folder name and filename are limited to 10 | 5 characters as SDFS requirements. 11 | */ 12 | 13 | //Root of the db on SD card, **must have tailing slash** 14 | const String DB_root = "/db/"; 15 | 16 | //Clean the input for any input string 17 | String DBCleanInput(const String& inputString) { 18 | String trimmedString = inputString; 19 | //Replae all the slash that might breaks the file system 20 | trimmedString.replace("/", ""); 21 | //Trim off the space before and after the string 22 | trimmedString.trim(); 23 | return trimmedString; 24 | } 25 | 26 | //Database init create all the required table for basic system operations 27 | void DBInit() { 28 | DBNewTable("auth"); 29 | DBNewTable("pref"); 30 | } 31 | 32 | //Create a new Database table 33 | void DBNewTable(String tableName) { 34 | tableName = DBCleanInput(tableName); 35 | if (!SD.exists(DB_root + tableName)) { 36 | SD.mkdir(DB_root + tableName); 37 | } 38 | } 39 | 40 | 41 | //Check if a database table exists 42 | bool DBTableExists(String tableName) { 43 | tableName = DBCleanInput(tableName); 44 | return SD.exists(DB_root + tableName); 45 | } 46 | 47 | //Write a key to a table, return true if succ 48 | bool DBWrite(String tableName, String key, String value) { 49 | if (!DBTableExists(tableName)) { 50 | return false; 51 | } 52 | tableName = DBCleanInput(tableName); 53 | key = DBCleanInput(key); 54 | String fsDataPath = DB_root + tableName + "/" + key; 55 | if (SD.exists(fsDataPath)) { 56 | //Entry already exists. Delete it 57 | SD.remove(fsDataPath); 58 | } 59 | 60 | //Write new data to it 61 | File targetEntry = SD.open(fsDataPath, FILE_WRITE); 62 | targetEntry.print(value); 63 | targetEntry.close(); 64 | return true; 65 | } 66 | 67 | //Read from database, require table name and key 68 | String DBRead(String tableName, String key) { 69 | if (!DBTableExists(tableName)) { 70 | return ""; 71 | } 72 | tableName = DBCleanInput(tableName); 73 | key = DBCleanInput(key); 74 | String fsDataPath = DB_root + tableName + "/" + key; 75 | if (!SD.exists(fsDataPath)) { 76 | //Target not exists. Return empty string 77 | return ""; 78 | } 79 | 80 | String value = ""; 81 | File targetEntry = SD.open(fsDataPath, FILE_READ); 82 | while (targetEntry.available()) { 83 | value = value + targetEntry.readString(); 84 | } 85 | 86 | targetEntry.close(); 87 | return value; 88 | } 89 | 90 | //Check if a given key exists in the database 91 | bool DBKeyExists(String tableName, String key) { 92 | if (!DBTableExists(tableName)) { 93 | return false; 94 | } 95 | tableName = DBCleanInput(tableName); 96 | key = DBCleanInput(key); 97 | String fsDataPath = DB_root + tableName + "/" + key; 98 | return SD.exists(fsDataPath); 99 | } 100 | 101 | //Remove the key-value item from db, return true if succ 102 | bool DBRemove(String tableName, String key) { 103 | if (!DBKeyExists(tableName, key)){ 104 | return false; 105 | } 106 | tableName = DBCleanInput(tableName); 107 | key = DBCleanInput(key); 108 | String fsDataPath = DB_root + tableName + "/" + key; 109 | SD.remove(fsDataPath); 110 | return true; 111 | } 112 | -------------------------------------------------------------------------------- /firmware/v2/web-server/router.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Router.ino 4 | 5 | This is the main router of the whole web server. 6 | It is like the apache.conf where you handle routing 7 | to different services. 8 | 9 | By default, all route will go to the SD card /www/ folder 10 | */ 11 | class MainRouter : public AsyncWebHandler { 12 | public: 13 | MainRouter() {} 14 | virtual ~MainRouter() {} 15 | 16 | bool canHandle(AsyncWebServerRequest *request) { 17 | String requestURI = request->url().c_str(); 18 | if (requestURI.equals("/upload")) { 19 | //File Upload Endpoint 20 | return false; 21 | } else if (requestURI.startsWith("/api/")) { 22 | //API paths 23 | return false; 24 | } 25 | return true; 26 | } 27 | 28 | //Main Routing Logic Here 29 | void handleRequest(AsyncWebServerRequest *request) { 30 | String requestURI = request->url().c_str(); 31 | 32 | /* Rewrite the request path if URI contains ./ */ 33 | if (requestURI.indexOf("./") > 0) { 34 | requestURI.replace("./", ""); 35 | AsyncWebServerResponse *response = request->beginResponse(307); 36 | response->addHeader("Cache-Control", "no-cache"); 37 | response->addHeader("Location", requestURI); 38 | request->send(response); 39 | return; 40 | } 41 | 42 | /* Special Routing Rules */ 43 | //Redirect / back to index.html 44 | if (requestURI == "/") { 45 | request->redirect("/index.html"); 46 | return; 47 | } 48 | 49 | //Special interfaces that require access controls 50 | if (requestURI.startsWith("/store/")) { 51 | //Private file storage. Not allow access 52 | AsyncWebServerResponse *response = request->beginResponse(401, "text/html", "403 - Forbidden"); 53 | request->send(response); 54 | return; 55 | } 56 | 57 | /* Default Routing Rules */ 58 | 59 | Serial.println("URI: " + requestURI + " | MIME: " + getMime(requestURI)); 60 | //Check if the file exists on the SD card 61 | if (SD.exists("/www" + requestURI)) { 62 | // File exists on SD card web root 63 | if (IsDir("/www" + requestURI)) { 64 | //Requesting a directory 65 | if (!requestURI.endsWith("/")) { 66 | //Missing tailing slash 67 | request->redirect(requestURI + "/"); 68 | return; 69 | } 70 | 71 | if (SD.exists("/www" + requestURI + "index.html")) { 72 | request->send(SDFS, "/www" + requestURI + "/index.html", "text/html", false); 73 | } else { 74 | HandleDirRender(request, requestURI , "/www" + requestURI); 75 | } 76 | 77 | } else { 78 | request->send(SDFS, "/www" + requestURI, getMime(requestURI), false); 79 | } 80 | } else { 81 | // File does not exist in web root 82 | AsyncResponseStream *response = request->beginResponseStream("text/html"); 83 | Serial.println("NOT FOUND: " + requestURI); 84 | prettyPrintRequest(request); 85 | response->print("Not Found"); 86 | response->print("

404 - Not Found

"); 87 | response->printf("

Requesting http://%s with URI: %s

", request->host().c_str(), request->url().c_str()); 88 | response->print(""); 89 | request->send(response); 90 | } 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /firmware/v2/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Match it to the server side value in kvdb 21 | //Serial.println(authCookie); //user cookie 22 | //Serial.println(authSession); //server session 23 | if (authSession == "") { 24 | //Server side has no resumable login session 25 | return false; 26 | } 27 | if (authCookie.equals(authSession)) { 28 | return true; 29 | } 30 | return false; 31 | } else { 32 | Serial.println("Cookie Missing"); 33 | return false; 34 | } 35 | } 36 | 37 | //Reply the request by a directory list 38 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 39 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 40 | //Serve directory entries 41 | File directory = SD.open(dirToList); 42 | 43 | // Check if the directory is open 44 | if (!directory) { 45 | SendErrorResp(r, "unable to open directory"); 46 | return; 47 | } 48 | 49 | response->print("Content of " + dirName + ""); 50 | response->print("

Content of " + dirName + "


Back"); 72 | response->print("

"); 73 | r->send(response); 74 | } 75 | 76 | void initWebServer() { 77 | /* 78 | Other handles here, like this 79 | server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){ 80 | request->send(401); 81 | }); 82 | */ 83 | 84 | /* 85 | server.on("/test", HTTP_GET, [](AsyncWebServerRequest * request) { 86 | getSDCardUsedSpace(); 87 | request->send(200); 88 | }); 89 | */ 90 | 91 | /* Authentication Functions */ 92 | server.on("/api/auth/chk", HTTP_GET, HandleCheckAuth); 93 | server.on("/api/auth/login", HTTP_POST, HandleLogin); 94 | server.on("/api/auth/logout", HTTP_GET, HandleLogout); 95 | 96 | /* File System Functions */ 97 | server.on("/api/fs/list", HTTP_GET, HandleListDir); 98 | server.on("/api/fs/del", HTTP_POST, HandleFileDel); 99 | server.on("/api/fs/move", HTTP_POST, HandleFileRename); 100 | server.on("/api/fs/download", HTTP_GET, HandleFileDownload); 101 | server.on("/api/fs/newFolder", HTTP_POST, HandleNewFolder); 102 | server.on("/api/fs/disk", HTTP_GET, HandleLoadSpaceInfo); 103 | server.on("/api/fs/properties", HTTP_GET, HandleFileProp); 104 | server.on("/api/fs/search", HTTP_GET, HandleFileSearch); 105 | 106 | /* Preference */ 107 | server.on("/api/pref/set", HTTP_GET, HandleSetPref); 108 | server.on("/api/pref/get", HTTP_GET, HandleLoadPref); 109 | 110 | /* Others */ 111 | server.on("/api/info/wifi", HTTP_GET, HandleWiFiInfo); 112 | 113 | //File upload handler. see upload.ino 114 | server.onFileUpload(handleFileUpload); 115 | 116 | //Not found handler 117 | server.onNotFound([](AsyncWebServerRequest * request) { 118 | //Generally it will not arrive here as NOT FOUND is also handled in the main router. 119 | //See router.ino for implementation details. 120 | prettyPrintRequest(request); 121 | request->send(404, "text/plain", "Not Found"); 122 | }); 123 | 124 | //Main Router, see router.ino 125 | server.addHandler(new MainRouter()); 126 | 127 | server.begin(); 128 | } 129 | -------------------------------------------------------------------------------- /firmware/v2/web-server/upload.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Upload.ino 4 | 5 | This script handles file upload to the web-stick 6 | by default this function require authentication. 7 | Hence, admin.txt must be set before use 8 | 9 | */ 10 | 11 | void handleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 12 | // make sure authenticated before allowing upload 13 | if (IsUserAuthed(request)) { 14 | String logmessage = ""; 15 | //String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); 16 | //Serial.println(logmessage); 17 | 18 | //Rewrite the filename if it is too long 19 | filename = trimFilename(filename); 20 | 21 | //Get the dir to store the file 22 | String dirToStore = GetPara(request, "dir"); 23 | if (!dirToStore.startsWith("/")) { 24 | dirToStore = "/" + dirToStore; 25 | } 26 | 27 | if (!dirToStore.endsWith("/")) { 28 | dirToStore = dirToStore + "/"; 29 | } 30 | dirToStore = "/www" + dirToStore; 31 | 32 | if (!index) { 33 | Serial.println("Selected Upload Dir: " + dirToStore); 34 | logmessage = "Upload Start: " + String(filename) + " by " + request->client()->remoteIP().toString(); 35 | // open the file on first call and store the file handle in the request object 36 | if (!SD.exists(dirToStore)) { 37 | SD.mkdir(dirToStore); 38 | } 39 | 40 | //Already exists. Overwrite 41 | if (SD.exists(dirToStore + filename)) { 42 | SD.remove(dirToStore + filename); 43 | } 44 | request->_tempFile = SD.open(dirToStore + filename, FILE_WRITE); 45 | Serial.println(logmessage); 46 | } 47 | 48 | if (len) { 49 | // stream the incoming chunk to the opened file 50 | request->_tempFile.write(data, len); 51 | //logmessage = "Writing file: " + String(filename) + " index=" + String(index) + " len=" + String(len); 52 | //Serial.println(logmessage); 53 | } 54 | 55 | if (final) { 56 | logmessage = "Upload Complete: " + String(filename) + ",size: " + String(index + len); 57 | // close the file handle as the upload is now done 58 | request->_tempFile.close(); 59 | Serial.println(logmessage); 60 | 61 | //Check if the file actually exists on SD card 62 | if (!SD.exists(String(dirToStore + filename))) { 63 | //Not found! 64 | SendErrorResp(request, "Write failed for " + String(filename) + ". Try a shorter name!"); 65 | return; 66 | } 67 | request->send(200, "application/json", "ok"); 68 | } 69 | } else { 70 | Serial.println("Auth: Failed"); 71 | SendErrorResp(request, "unauthorized"); 72 | } 73 | } 74 | 75 | /* 76 | Upload File Trimming 77 | 78 | This trim the given uploading filename to less than 32 chars 79 | if the filename is too long to fit on the SD card. 80 | 81 | The code handle UTF-8 trimming at the bytes level. 82 | */ 83 | 84 | //UTF-8 is varaible in length, this get how many bytes in the coming sequences 85 | //are part of this UTF-8 word 86 | uint8_t getUtf8CharLength(const uint8_t firstByte) { 87 | if ((firstByte & 0x80) == 0) { 88 | // Single-byte character 89 | return 1; 90 | } else if ((firstByte & 0xE0) == 0xC0) { 91 | // Two-byte character 92 | return 2; 93 | } else if ((firstByte & 0xF0) == 0xE0) { 94 | // Three-byte character 95 | return 3; 96 | } else if ((firstByte & 0xF8) == 0xF0) { 97 | // Four-byte character 98 | return 4; 99 | } else { 100 | // Invalid UTF-8 character 101 | return 0; 102 | } 103 | } 104 | 105 | String filterBrokenUtf8(const String& input) { 106 | String result; 107 | size_t inputLength = input.length(); 108 | size_t i = 0; 109 | while (i < inputLength) { 110 | uint8_t firstByte = input[i]; 111 | uint8_t charLength = getUtf8CharLength(firstByte); 112 | 113 | if (charLength == 0){ 114 | //End of filter 115 | break; 116 | } 117 | 118 | // Check if the character is complete (non-broken UTF-8) 119 | if (i + charLength <= inputLength) { 120 | // Check for invalid UTF-8 continuation bytes in the character 121 | for (size_t j = 0; j < charLength; j++) { 122 | result += input[i]; 123 | i++; 124 | } 125 | }else{ 126 | //End of valid UTF-8 segment 127 | break; 128 | } 129 | } 130 | return result; 131 | } 132 | 133 | String trimFilename(String& filename) { 134 | //Replace all things that is not suppose to be in the filename 135 | filename.replace("#",""); 136 | filename.replace("?",""); 137 | filename.replace("&",""); 138 | 139 | // Find the position of the last dot (file extension) 140 | int dotIndex = filename.lastIndexOf('.'); 141 | 142 | // Check if the filename contains a dot and the extension is not at the beginning or end 143 | if (dotIndex > 0 && dotIndex < filename.length() - 1) { 144 | // Calculate the maximum length for the filename (excluding extension) 145 | int maxLength = 32 - (filename.length() - dotIndex - 1); 146 | 147 | // Truncate the filename if it's longer than the maximum length 148 | if (filename.length() > maxLength) { 149 | String trimmedFilename = filterBrokenUtf8(filename.substring(0, maxLength)) + filename.substring(dotIndex); 150 | return trimmedFilename; 151 | } 152 | } 153 | 154 | // If no truncation is needed, return the original filename 155 | return filename; 156 | } 157 | -------------------------------------------------------------------------------- /firmware/v2/web-server/web-server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Web Server Stick 4 | * Author: Toby Chui 5 | * 6 | * This firmware load and serve web content 7 | * from microSD card. 8 | * 9 | * The following firmware config are recommended 10 | * Board: Wemos D1 Mini 11 | * CPU clockspeed: 160Mhz 12 | * IwIP Varient: v2 Higher Bandwidth 13 | * 14 | * Require external library: 15 | * - ESPAsyncTCP (https://github.com/me-no-dev/ESPAsyncTCP) 16 | * - ESPAsyncWebServer (https://github.com/me-no-dev/ESPAsyncWebServer) 17 | * - ArduinoJson (https://github.com/bblanchon/ArduinoJson) 18 | */ 19 | 20 | //WiFi library 21 | #include 22 | 23 | //SD cards library 24 | #include 25 | #include 26 | #include 27 | 28 | //Web server library 29 | #include 30 | #include 31 | #include 32 | 33 | //Discovery related library 34 | #include 35 | #include 36 | #include 37 | 38 | /* Hardware Configurations */ 39 | #define CS_PIN D0 40 | 41 | /* Software Global Variables */ 42 | AsyncWebServer server(80); 43 | String adminUsername = ""; 44 | String adminPassword = ""; 45 | String mdnsName = "webstick"; 46 | String authSession = ""; 47 | 48 | /* Time Keeping */ 49 | WiFiUDP ntpUDP; 50 | NTPClient timeClient(ntpUDP, "pool.ntp.org"); 51 | 52 | /* Debug variables */ 53 | 54 | /* Function definations */ 55 | String loadWiFiInfoFromSD(); 56 | 57 | 58 | void setup() { 59 | // Setup Debug Serial Port 60 | Serial.begin(9600); 61 | 62 | //Try Initialize SD card (blocking) 63 | while (!SD.begin(CS_PIN, 32000000)){ 64 | Serial.println("SD card initialization failed. Retrying in 3 seconds..."); 65 | delay(3000); 66 | } 67 | Serial.println("SD card initialized"); 68 | Serial.println("\n\nStorage Info:"); 69 | Serial.println("----------------------"); 70 | getSDCardTotalSpace(); 71 | getSDCardUsedSpace(); 72 | Serial.println("----------------------"); 73 | Serial.println(); 74 | 75 | //Connect to wifi based on settings (cfg/wifi.txt) 76 | initWiFiConn(); 77 | 78 | //Load admin credentials from SD card (cfg/admin.txt) 79 | initAdminCredentials(); 80 | 81 | //Start mDNS service 82 | initmDNSName(); 83 | if (!MDNS.begin(mdnsName)){ 84 | Serial.println("mDNS Error. Skipping."); 85 | }else{ 86 | Serial.println("mDNS started. Connect to your webstick using http://" + mdnsName + ".local"); 87 | MDNS.addService("http", "tcp", 80); 88 | } 89 | 90 | //Start NTP time client 91 | timeClient.begin(); 92 | Serial.print("Requesting time from NTP (unix timestamp): "); 93 | timeClient.update(); 94 | Serial.println(getTime()); 95 | 96 | //Initialize database 97 | DBInit(); 98 | 99 | //Resume login session if any 100 | initLoginSessionKey(); 101 | 102 | // Start listening to HTTP Requests 103 | initWebServer(); 104 | } 105 | 106 | 107 | void loop(){ 108 | MDNS.update(); 109 | timeClient.update(); 110 | } 111 | -------------------------------------------------------------------------------- /firmware/v3/web-server/kvdb.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Key Value database 4 | 5 | This is a file system based database 6 | that uses foldername as table name, 7 | filename as key and content as value 8 | 9 | Folder name and filename are limited to 10 | 5 characters as SDFS requirements. 11 | */ 12 | 13 | //Root of the db on SD card, **must have tailing slash** 14 | const String DB_root = "/db/"; 15 | 16 | //Clean the input for any input string 17 | String DBCleanInput(const String& inputString) { 18 | String trimmedString = inputString; 19 | //Replae all the slash that might breaks the file system 20 | trimmedString.replace("/", ""); 21 | //Trim off the space before and after the string 22 | trimmedString.trim(); 23 | return trimmedString; 24 | } 25 | 26 | //Database init create all the required table for basic system operations 27 | void DBInit() { 28 | /* Preference persistent store */ 29 | DBNewTable("pref"); //Preference settings 30 | /* User Authentications Tables */ 31 | DBNewTable("auth"); //Auth session store 32 | DBNewTable("user"); //User accounts store 33 | DBNewTable("sess"); //Session store 34 | /* Share System Tables */ 35 | DBNewTable("shln");//Shared links to filename map 36 | DBNewTable("shfn");//Shared filename to links map 37 | } 38 | 39 | //Create a new Database table 40 | void DBNewTable(String tableName) { 41 | tableName = DBCleanInput(tableName); 42 | if (!SD.exists(DB_root + tableName)) { 43 | SD.mkdir(DB_root + tableName); 44 | } 45 | } 46 | 47 | 48 | //Check if a database table exists 49 | bool DBTableExists(String tableName) { 50 | tableName = DBCleanInput(tableName); 51 | return SD.exists(DB_root + tableName); 52 | } 53 | 54 | //Write a key to a table, return true if succ 55 | bool DBWrite(String tableName, String key, String value) { 56 | if (!DBTableExists(tableName)) { 57 | return false; 58 | } 59 | tableName = DBCleanInput(tableName); 60 | key = DBCleanInput(key); 61 | String fsDataPath = DB_root + tableName + "/" + key; 62 | if (SD.exists(fsDataPath)) { 63 | //Entry already exists. Delete it 64 | SD.remove(fsDataPath); 65 | } 66 | 67 | //Write new data to it 68 | File targetEntry = SD.open(fsDataPath, FILE_WRITE); 69 | targetEntry.print(value); 70 | targetEntry.close(); 71 | return true; 72 | } 73 | 74 | //Read from database, require table name and key 75 | String DBRead(String tableName, String key) { 76 | if (!DBTableExists(tableName)) { 77 | return ""; 78 | } 79 | tableName = DBCleanInput(tableName); 80 | key = DBCleanInput(key); 81 | String fsDataPath = DB_root + tableName + "/" + key; 82 | if (!SD.exists(fsDataPath)) { 83 | //Target not exists. Return empty string 84 | return ""; 85 | } 86 | 87 | String value = ""; 88 | File targetEntry = SD.open(fsDataPath, FILE_READ); 89 | while (targetEntry.available()) { 90 | value = value + targetEntry.readString(); 91 | } 92 | 93 | targetEntry.close(); 94 | return value; 95 | } 96 | 97 | //Check if a given key exists in the database 98 | bool DBKeyExists(String tableName, String key) { 99 | if (!DBTableExists(tableName)) { 100 | return false; 101 | } 102 | tableName = DBCleanInput(tableName); 103 | key = DBCleanInput(key); 104 | String fsDataPath = DB_root + tableName + "/" + key; 105 | return SD.exists(fsDataPath); 106 | } 107 | 108 | //Remove the key-value item from db, return true if succ 109 | bool DBRemove(String tableName, String key) { 110 | if (!DBKeyExists(tableName, key)){ 111 | return false; 112 | } 113 | tableName = DBCleanInput(tableName); 114 | key = DBCleanInput(key); 115 | String fsDataPath = DB_root + tableName + "/" + key; 116 | SD.remove(fsDataPath); 117 | return true; 118 | } 119 | -------------------------------------------------------------------------------- /firmware/v3/web-server/lib/WakeOnLan.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v3/web-server/lib/WakeOnLan.zip -------------------------------------------------------------------------------- /firmware/v3/web-server/router.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Router.ino 4 | 5 | This is the main router of the whole web server. 6 | It is like the apache.conf where you handle routing 7 | to different services. 8 | 9 | By default, all route will go to the SD card /www/ folder 10 | */ 11 | class MainRouter : public AsyncWebHandler { 12 | public: 13 | MainRouter() {} 14 | virtual ~MainRouter() {} 15 | 16 | bool canHandle(AsyncWebServerRequest *request) { 17 | String requestURI = request->url().c_str(); 18 | if (requestURI.equals("/upload")) { 19 | //File Upload Endpoint 20 | return false; 21 | } else if (requestURI.startsWith("/api/")) { 22 | //API paths 23 | return false; 24 | }else if (requestURI.startsWith("/share/")) { 25 | //Share paths 26 | return false; 27 | } 28 | return true; 29 | } 30 | 31 | //Main Routing Logic Here 32 | void handleRequest(AsyncWebServerRequest *request) { 33 | String requestURI = request->url().c_str(); 34 | 35 | /* Rewrite the request path if URI contains ./ */ 36 | if (requestURI.indexOf("./") > 0) { 37 | requestURI.replace("./", ""); 38 | AsyncWebServerResponse *response = request->beginResponse(307); 39 | response->addHeader("Cache-Control", "no-cache"); 40 | response->addHeader("Location", requestURI); 41 | request->send(response); 42 | return; 43 | } 44 | 45 | /* Special Routing Rules */ 46 | //Redirect / back to index.html 47 | if (requestURI == "/") { 48 | request->redirect("/index.html"); 49 | return; 50 | } 51 | 52 | //Special interfaces that require access controls 53 | if (requestURI.startsWith("/store/")) { 54 | //Private file storage. Not allow access 55 | AsyncWebServerResponse *response = request->beginResponse(401, "text/html", "403 - Forbidden"); 56 | request->send(response); 57 | return; 58 | } 59 | 60 | /* Default Routing Rules */ 61 | 62 | Serial.println("URI: " + requestURI + " | MIME: " + getMime(requestURI)); 63 | //Check if the file exists on the SD card 64 | if (SD.exists("/www" + requestURI)) { 65 | // File exists on SD card web root 66 | if (IsDir("/www" + requestURI)) { 67 | //Requesting a directory 68 | if (!requestURI.endsWith("/")) { 69 | //Missing tailing slash 70 | request->redirect(requestURI + "/"); 71 | return; 72 | } 73 | 74 | if (SD.exists("/www" + requestURI + "index.html")) { 75 | request->send(SDFS, "/www" + requestURI + "/index.html", "text/html", false); 76 | } else { 77 | HandleDirRender(request, requestURI , "/www" + requestURI); 78 | } 79 | 80 | } else { 81 | request->send(SDFS, "/www" + requestURI, getMime(requestURI), false); 82 | } 83 | } else { 84 | // File does not exist in web root 85 | AsyncResponseStream *response = request->beginResponseStream("text/html"); 86 | Serial.println("NOT FOUND: " + requestURI); 87 | prettyPrintRequest(request); 88 | response->print("Not Found"); 89 | response->print("

404 - Not Found

"); 90 | response->printf("

Requesting http://%s with URI: %s

", request->host().c_str(), request->url().c_str()); 91 | response->print(""); 92 | request->send(response); 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /firmware/v3/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Match it to the server side value in kvdb 21 | if (authSession == "") { 22 | //Server side has no resumable login session 23 | return false; 24 | } 25 | if (authCookie.equals(authSession)) { 26 | //Admin login 27 | return true; 28 | }else if (DBKeyExists("sess", authCookie)){ 29 | //User login 30 | return true; 31 | } 32 | 33 | return false; 34 | } else { 35 | Serial.println("Cookie Missing"); 36 | return false; 37 | } 38 | } 39 | 40 | //Check if a user is authenticated and is Admin 41 | bool IsAdmin(AsyncWebServerRequest *request) { 42 | if (request->hasHeader("Cookie")) { 43 | //User cookie from browser 44 | String authCookie = GetCookieValueByKey(request, "web-auth"); 45 | if (authCookie == "") { 46 | return false; 47 | } 48 | 49 | //Match it to the server side value in kvdb 50 | if (authSession == "") { 51 | //Server side has no resumable login session 52 | return false; 53 | } 54 | if (authCookie.equals(authSession)) { 55 | return true; 56 | } 57 | return false; 58 | } else { 59 | return false; 60 | } 61 | } 62 | 63 | 64 | //Reply the request by a directory list 65 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 66 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 67 | //Serve directory entries 68 | File directory = SD.open(dirToList); 69 | 70 | // Check if the directory is open 71 | if (!directory) { 72 | SendErrorResp(r, "unable to open directory"); 73 | return; 74 | } 75 | 76 | response->print("Content of " + dirName + ""); 77 | response->print("

Content of " + dirName + "


Back"); 99 | response->print("

"); 100 | r->send(response); 101 | } 102 | 103 | 104 | void initWebServer() { 105 | /* 106 | Other handles here, like this 107 | server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){ 108 | request->send(401); 109 | }); 110 | */ 111 | 112 | /* 113 | server.on("/test", HTTP_GET, [](AsyncWebServerRequest * request) { 114 | getSDCardUsedSpace(); 115 | request->send(200); 116 | }); 117 | */ 118 | 119 | /* Authentication Functions */ 120 | server.on("/api/auth/chk", HTTP_GET, HandleCheckAuth); 121 | server.on("/api/auth/login", HTTP_POST, HandleLogin); 122 | server.on("/api/auth/logout", HTTP_GET, HandleLogout); 123 | 124 | /* User System Functions */ 125 | server.on("/api/user/info", HTTP_GET, HandleGetUserinfo); 126 | server.on("/api/user/new", HTTP_POST, HandleNewUser); 127 | server.on("/api/user/chpw", HTTP_POST, HandleUserChangePassword); 128 | server.on("/api/user/del", HTTP_POST, HandleRemoveUser); 129 | server.on("/api/user/list", HTTP_GET, HandleUserList); 130 | 131 | /* File System Functions */ 132 | server.on("/api/fs/list", HTTP_GET, HandleListDir); 133 | server.on("/api/fs/del", HTTP_POST, HandleFileDel); 134 | server.on("/api/fs/move", HTTP_POST, HandleFileRename); 135 | server.on("/api/fs/download", HTTP_GET, HandleFileDownload); 136 | server.on("/api/fs/newFolder", HTTP_POST, HandleNewFolder); 137 | server.on("/api/fs/disk", HTTP_GET, HandleLoadSpaceInfo); 138 | server.on("/api/fs/properties", HTTP_GET, HandleFileProp); 139 | server.on("/api/fs/search", HTTP_GET, HandleFileSearch); 140 | 141 | /* File Share Functions */ 142 | server.on("/api/share/new", HTTP_POST, HandleCreateShare); 143 | server.on("/api/share/del", HTTP_POST, HandleRemoveShare); 144 | server.on("/api/share/list", HTTP_GET, HandleShareList); 145 | server.on("/api/share/clean", HTTP_GET, HandleShareListCleaning); 146 | server.on("/share", HTTP_GET, HandleShareAccess); 147 | 148 | 149 | /* Preference */ 150 | server.on("/api/pref/set", HTTP_GET, HandleSetPref); 151 | server.on("/api/pref/get", HTTP_GET, HandleLoadPref); 152 | 153 | /* Others */ 154 | server.on("/api/info/wifi", HTTP_GET, HandleWiFiInfo); //Show WiFi Information 155 | server.on("/api/wol", HTTP_GET, HandleWakeOnLan); //Handle WoL request 156 | 157 | //File upload handler. see upload.ino 158 | server.onFileUpload(handleFileUpload); 159 | 160 | //Not found handler 161 | server.onNotFound([](AsyncWebServerRequest *request) { 162 | //Generally it will not arrive here as NOT FOUND is also handled in the main router. 163 | //See router.ino for implementation details. 164 | prettyPrintRequest(request); 165 | request->send(404, "text/plain", "Not Found"); 166 | }); 167 | 168 | //Main Router, see router.ino 169 | server.addHandler(new MainRouter()); 170 | 171 | server.begin(); 172 | } 173 | -------------------------------------------------------------------------------- /firmware/v3/web-server/share.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Share.ino 3 | 4 | This module handle file sharing on the WebSticks 5 | Recommended file size <= 5MB 6 | 7 | */ 8 | 9 | //Create a file share, must be logged in 10 | void HandleCreateShare(AsyncWebServerRequest *r) { 11 | if (!HandleAuth(r)) { 12 | return; 13 | } 14 | 15 | //Get filename from parameters 16 | String filepath = GetPara(r, "filename"); 17 | filepath.trim(); 18 | //filepath is the subpath under the www folder 19 | // e.g. "/www/myfile.txt" filepath will be "/myfile.txt" 20 | if (filepath == "") { 21 | SendErrorResp(r, "invalid filename given"); 22 | return; 23 | } 24 | 25 | if (IsFileShared(filepath)) { 26 | SendErrorResp(r, "target file already shared"); 27 | return; 28 | } 29 | 30 | //Add a share entry for this file 31 | String shareID = GeneratedRandomHex(); 32 | bool succ = DBWrite("shln", shareID, filepath); 33 | if (!succ) { 34 | SendErrorResp(r, "unable to save share entry"); 35 | return; 36 | } 37 | succ = DBWrite("shfn", filepath, shareID); 38 | if (!succ) { 39 | SendErrorResp(r, "unable to save share entry"); 40 | return; 41 | } 42 | 43 | Serial.println("Shared: " + filepath + " with ID: " + shareID); 44 | SendOK(r); 45 | } 46 | 47 | //Remove a file share 48 | void HandleRemoveShare(AsyncWebServerRequest *r) { 49 | if (!HandleAuth(r)) { 50 | return; 51 | } 52 | 53 | //Get filename from parameters 54 | String filepath = GetPara(r, "filename"); 55 | filepath.trim(); 56 | if (filepath == "") { 57 | SendErrorResp(r, "invalid filename given"); 58 | return; 59 | } 60 | 61 | if (!IsFileShared(filepath)) { 62 | SendErrorResp(r, "target file is not shared"); 63 | return; 64 | } 65 | 66 | //Get the share ID of this entry 67 | String shareId = DBRead("shfn", filepath); 68 | if (shareId == "") { 69 | SendErrorResp(r, "unable to load share entry"); 70 | return; 71 | } 72 | 73 | //Remove share entry in both tables 74 | bool succ = DBRemove("shln", shareId); 75 | if (!succ) { 76 | SendErrorResp(r, "unable to remove share entry"); 77 | return; 78 | } 79 | succ = DBRemove("shfn", filepath); 80 | if (!succ) { 81 | SendErrorResp(r, "unable to remove share entry"); 82 | return; 83 | } 84 | 85 | Serial.println("Removed shared file " + filepath + " (share ID: " + shareId + ")"); 86 | SendOK(r); 87 | } 88 | 89 | //List all shared files 90 | void HandleShareList(AsyncWebServerRequest *r) { 91 | if (!HandleAuth(r)) { 92 | return; 93 | } 94 | 95 | //Build the json with brute force 96 | String jsonString = "["; 97 | //As the DB do not support list, it directly access the root of the folder where the kvdb stores the entries 98 | File root = SD.open(DB_root + "shln/"); 99 | bool firstObject = true; 100 | if (root) { 101 | while (true) { 102 | File entry = root.openNextFile(); 103 | if (!entry) { 104 | // No more files 105 | break; 106 | } else { 107 | //There are more lines. Add a , to the end of the previous json object 108 | if (!firstObject) { 109 | jsonString = jsonString + ","; 110 | } else { 111 | firstObject = false; 112 | } 113 | 114 | //Filter out all the directory if any 115 | if (entry.isDirectory()) { 116 | continue; 117 | } 118 | 119 | //Read the filename from file 120 | String filename = ""; 121 | while (entry.available()) { 122 | filename = filename + entry.readString(); 123 | } 124 | 125 | //Append to the JSON line 126 | jsonString = jsonString + "{\"filename\":\"" + basename(filename) + "\", \"filepath\":\"" + filename + "\", \"shareid\":\"" + entry.name() + "\"}"; 127 | } 128 | } 129 | } 130 | jsonString += "]"; 131 | 132 | r->send(200, "application/json", jsonString); 133 | } 134 | 135 | //Serve a shared file, do not require login 136 | void HandleShareAccess(AsyncWebServerRequest *r) { 137 | String shareID = GetPara(r, "id"); 138 | if (shareID == "") { 139 | r->send(404, "text/plain", "Not Found"); 140 | return; 141 | } 142 | 143 | //Download request 144 | String sharedFilename = GetFilenameFromShareID(shareID); 145 | if (sharedFilename == "") { 146 | r->send(404, "text/plain", "Share not found"); 147 | return; 148 | } 149 | 150 | //Check if the file still exists on SD card 151 | String realFilepath = "/www" + sharedFilename; 152 | File targetFile = SD.open(realFilepath); 153 | if (!targetFile) { 154 | r->send(404, "text/plain", "Shared file no longer exists"); 155 | return;; 156 | } 157 | 158 | 159 | if (r->hasParam("download")) { 160 | //Serve the file 161 | r->send(SDFS, realFilepath, getMime(sharedFilename), false); 162 | } else if (r->hasParam("prop")) { 163 | //Serve the file properties 164 | File targetFile = SD.open(realFilepath); 165 | if (!targetFile) { 166 | SendErrorResp(r, "File open failed"); 167 | return; 168 | } 169 | 170 | String resp = "{\"filename\":\"" + basename(sharedFilename) + "\",\"filepath\":\"" + sharedFilename + "\",\"isDir\":false,\"filesize\":" + String(targetFile.size()) + ",\"shareid\":\"" + shareID + "\"}"; 171 | targetFile.close(); 172 | SendJsonResp(r, resp); 173 | } else { 174 | //serve download UI template 175 | r->send(SDFS, "/www/admin/share.html", "text/html", false); 176 | return; 177 | } 178 | } 179 | 180 | //Clear the shares that no longer exists 181 | void HandleShareListCleaning(AsyncWebServerRequest *r) { 182 | if (!HandleAuth(r)) { 183 | return; 184 | } 185 | 186 | File root = SD.open(DB_root + "shln/"); 187 | bool firstObject = true; 188 | if (root) { 189 | while (true) { 190 | File entry = root.openNextFile(); 191 | if (!entry) { 192 | // No more files 193 | break; 194 | } else { 195 | //Filter out all the directory if any 196 | if (entry.isDirectory()) { 197 | continue; 198 | } 199 | 200 | //Read the filename from file 201 | String filename = ""; 202 | while (entry.available()) { 203 | filename = filename + entry.readString(); 204 | } 205 | 206 | //Check if the target file still exists 207 | File targetFile = SD.open("/www" + filename); 208 | if (!targetFile) { 209 | //File no longer exists. Remove this share entry 210 | DBRemove("shln", entry.name()); 211 | DBRemove("shfn", filename); 212 | } else { 213 | //File still exists. 214 | targetFile.close(); 215 | } 216 | } 217 | } 218 | } 219 | 220 | SendOK(r); 221 | } 222 | 223 | 224 | //Get the file share ID from filename, return empty string if not shared 225 | String GetFileShareIDByFilename(String filepath) { 226 | return DBRead("shfn", filepath); 227 | } 228 | 229 | //Get the filename (without /www prefix) from share id 230 | // return empty string if not found 231 | String GetFilenameFromShareID(String shareid) { 232 | return DBRead("shln", shareid); 233 | } 234 | 235 | 236 | //Check if a file is shared 237 | bool IsFileShared(String filepath) { 238 | //Check if the file is shared 239 | return DBKeyExists("shfn", filepath); 240 | } 241 | -------------------------------------------------------------------------------- /firmware/v3/web-server/upload.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Upload.ino 4 | 5 | This script handles file upload to the web-stick 6 | by default this function require authentication. 7 | Hence, admin.txt must be set before use 8 | 9 | */ 10 | 11 | void handleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 12 | // make sure authenticated before allowing upload 13 | if (IsUserAuthed(request)) { 14 | String logmessage = ""; 15 | //String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); 16 | //Serial.println(logmessage); 17 | 18 | //Rewrite the filename if it is too long 19 | filename = trimFilename(filename); 20 | 21 | //Get the dir to store the file 22 | String dirToStore = GetPara(request, "dir"); 23 | if (!dirToStore.startsWith("/")) { 24 | dirToStore = "/" + dirToStore; 25 | } 26 | 27 | if (!dirToStore.endsWith("/")) { 28 | dirToStore = dirToStore + "/"; 29 | } 30 | dirToStore = "/www" + dirToStore; 31 | 32 | if (!index) { 33 | Serial.println("Selected Upload Dir: " + dirToStore); 34 | logmessage = "Upload Start: " + String(filename) + " by " + request->client()->remoteIP().toString(); 35 | // open the file on first call and store the file handle in the request object 36 | if (!SD.exists(dirToStore)) { 37 | SD.mkdir(dirToStore); 38 | } 39 | 40 | //Already exists. Overwrite 41 | if (SD.exists(dirToStore + filename)) { 42 | SD.remove(dirToStore + filename); 43 | } 44 | request->_tempFile = SD.open(dirToStore + filename, FILE_WRITE); 45 | Serial.println(logmessage); 46 | } 47 | 48 | if (len) { 49 | // stream the incoming chunk to the opened file 50 | request->_tempFile.write(data, len); 51 | //logmessage = "Writing file: " + String(filename) + " index=" + String(index) + " len=" + String(len); 52 | //Serial.println(logmessage); 53 | } 54 | 55 | if (final) { 56 | logmessage = "Upload Complete: " + String(filename) + ",size: " + String(index + len); 57 | // close the file handle as the upload is now done 58 | request->_tempFile.close(); 59 | Serial.println(logmessage); 60 | 61 | //Check if the file actually exists on SD card 62 | if (!SD.exists(String(dirToStore + filename))) { 63 | //Not found! 64 | SendErrorResp(request, "Write failed for " + String(filename) + ". Try a shorter name!"); 65 | return; 66 | } 67 | request->send(200, "application/json", "ok"); 68 | } 69 | } else { 70 | Serial.println("Auth: Failed"); 71 | SendErrorResp(request, "unauthorized"); 72 | } 73 | } 74 | 75 | /* 76 | Upload File Trimming 77 | 78 | This trim the given uploading filename to less than 32 chars 79 | if the filename is too long to fit on the SD card. 80 | 81 | The code handle UTF-8 trimming at the bytes level. 82 | */ 83 | 84 | //UTF-8 is varaible in length, this get how many bytes in the coming sequences 85 | //are part of this UTF-8 word 86 | uint8_t getUtf8CharLength(const uint8_t firstByte) { 87 | if ((firstByte & 0x80) == 0) { 88 | // Single-byte character 89 | return 1; 90 | } else if ((firstByte & 0xE0) == 0xC0) { 91 | // Two-byte character 92 | return 2; 93 | } else if ((firstByte & 0xF0) == 0xE0) { 94 | // Three-byte character 95 | return 3; 96 | } else if ((firstByte & 0xF8) == 0xF0) { 97 | // Four-byte character 98 | return 4; 99 | } else { 100 | // Invalid UTF-8 character 101 | return 0; 102 | } 103 | } 104 | 105 | String filterBrokenUtf8(const String& input) { 106 | String result; 107 | size_t inputLength = input.length(); 108 | size_t i = 0; 109 | while (i < inputLength) { 110 | uint8_t firstByte = input[i]; 111 | uint8_t charLength = getUtf8CharLength(firstByte); 112 | 113 | if (charLength == 0){ 114 | //End of filter 115 | break; 116 | } 117 | 118 | // Check if the character is complete (non-broken UTF-8) 119 | if (i + charLength <= inputLength) { 120 | // Check for invalid UTF-8 continuation bytes in the character 121 | for (size_t j = 0; j < charLength; j++) { 122 | result += input[i]; 123 | i++; 124 | } 125 | }else{ 126 | //End of valid UTF-8 segment 127 | break; 128 | } 129 | } 130 | return result; 131 | } 132 | 133 | String trimFilename(String& filename) { 134 | //Replace all things that is not suppose to be in the filename 135 | filename.replace("#",""); 136 | filename.replace("?",""); 137 | filename.replace("&",""); 138 | 139 | // Find the position of the last dot (file extension) 140 | int dotIndex = filename.lastIndexOf('.'); 141 | 142 | // Check if the filename contains a dot and the extension is not at the beginning or end 143 | if (dotIndex > 0 && dotIndex < filename.length() - 1) { 144 | // Calculate the maximum length for the filename (excluding extension) 145 | int maxLength = 32 - (filename.length() - dotIndex - 1); 146 | 147 | // Truncate the filename if it's longer than the maximum length 148 | if (filename.length() > maxLength) { 149 | String trimmedFilename = filterBrokenUtf8(filename.substring(0, maxLength)) + filename.substring(dotIndex); 150 | return trimmedFilename; 151 | } 152 | } 153 | 154 | // If no truncation is needed, return the original filename 155 | return filename; 156 | } 157 | -------------------------------------------------------------------------------- /firmware/v3/web-server/users.ino: -------------------------------------------------------------------------------- 1 | /* 2 | User.ino 3 | 4 | This is a new module handling user systems on ESP8266 5 | 6 | */ 7 | 8 | //Check if a user login is valid by username and password 9 | bool UserCheckAuth(String username, String password) { 10 | username.trim(); 11 | password.trim(); 12 | //Load user info from db 13 | if (!DBKeyExists("user", username)) { 14 | return false; 15 | } 16 | 17 | String userHashedPassword = DBRead("user", username); //User hashed password from kvdb 18 | String enteredHashedPassword = sha1(password); //Entered hashed password 19 | return userHashedPassword.equals(enteredHashedPassword); 20 | } 21 | 22 | //Get the username from the request, return empty string if unable to resolve 23 | String GetUsernameFromRequest(AsyncWebServerRequest *r) { 24 | if (r->hasHeader("Cookie")) { 25 | //User cookie from browser 26 | String authCookie = GetCookieValueByKey(r, "web-auth"); 27 | if (authCookie == "") { 28 | return ""; 29 | } 30 | 31 | //Check if this is admin login 32 | if (authCookie.equals(authSession)) { 33 | return adminUsername; 34 | } 35 | 36 | //Check if user login 37 | if (DBKeyExists("sess", authCookie)) { 38 | //Return the username of this session 39 | return DBRead("sess", authCookie); 40 | } 41 | 42 | //Not found 43 | return ""; 44 | } 45 | 46 | //This user have no cookie in header 47 | return ""; 48 | } 49 | 50 | //Create new user, creator must be admin 51 | void HandleNewUser(AsyncWebServerRequest *r) { 52 | if (!IsAdmin(r)) { 53 | SendErrorResp(r, "this function require admin permission"); 54 | return; 55 | } 56 | String username = GetPara(r, "username"); 57 | String password = GetPara(r, "password"); 58 | username.trim(); 59 | password.trim(); 60 | 61 | //Check if the inputs are valid 62 | if (username == "" || password == "") { 63 | SendErrorResp(r, "username or password is an empty string"); 64 | return; 65 | } else if (password.length() < 8) { 66 | SendErrorResp(r, "password must contain at least 8 characters"); 67 | return; 68 | } 69 | 70 | //Check if the user already exists 71 | if (DBKeyExists("user", username)) { 72 | SendErrorResp(r, "user with name: " + username + " already exists"); 73 | return; 74 | } 75 | 76 | //OK create the user 77 | bool succ = DBWrite("user", username, sha1(password)); 78 | if (!succ) { 79 | SendErrorResp(r, "write new user to database failed"); 80 | return; 81 | } 82 | r->send(200, "application/json", "\"OK\""); 83 | } 84 | 85 | //Remove the given username from the system 86 | void HandleRemoveUser(AsyncWebServerRequest *r) { 87 | if (!IsAdmin(r)) { 88 | SendErrorResp(r, "this function require admin permission"); 89 | return; 90 | } 91 | 92 | String username = GetPara(r, "username"); 93 | username.trim(); 94 | 95 | //Check if the user exists 96 | if (!DBKeyExists("user", username)) { 97 | SendErrorResp(r, "user with name: " + username + " not exists"); 98 | return; 99 | } 100 | 101 | //Okey, remove the user 102 | bool succ = DBRemove("user", username); 103 | if (!succ) { 104 | SendErrorResp(r, "remove user from system failed"); 105 | return; 106 | } 107 | r->send(200, "application/json", "\"OK\""); 108 | } 109 | 110 | //Admin or the user themselve change password for the account 111 | void HandleUserChangePassword(AsyncWebServerRequest *r) { 112 | //Get requesting username 113 | if (!IsUserAuthed(r)) { 114 | SendErrorResp(r, "user not logged in"); 115 | return; 116 | } 117 | 118 | String currentUser = GetUsernameFromRequest(r); 119 | if (currentUser == "") { 120 | SendErrorResp(r, "unable to load user from system"); 121 | return; 122 | } 123 | 124 | //Check if the user can change password 125 | //note that admin password cannot be changed on-the-fly 126 | //admin password can only be changed in SD card config file 127 | String modifyingUsername = GetPara(r, "username"); 128 | String newPassword = GetPara(r, "newpw"); 129 | modifyingUsername.trim(); 130 | newPassword.trim(); 131 | 132 | if (modifyingUsername == adminUsername) { 133 | SendErrorResp(r, "admin username can only be changed in the config file"); 134 | return; 135 | } 136 | if (currentUser == adminUsername || modifyingUsername == currentUser) { 137 | //Allow modify 138 | if (newPassword.length() < 8) { 139 | SendErrorResp(r, "password must contain at least 8 characters"); 140 | return; 141 | } 142 | 143 | //Write to database 144 | bool succ = DBWrite("user", modifyingUsername, sha1(newPassword)); 145 | if (!succ) { 146 | SendErrorResp(r, "write new user to database failed"); 147 | return; 148 | } 149 | SendOK(r); 150 | 151 | } else { 152 | SendErrorResp(r, "permission denied"); 153 | return; 154 | } 155 | 156 | SendOK(r); 157 | } 158 | 159 | //Get the current username 160 | void HandleGetUserinfo(AsyncWebServerRequest *r){ 161 | if (!HandleAuth(r)) { 162 | return; 163 | } 164 | 165 | String isAdmin = "false"; 166 | if (IsAdmin(r)){ 167 | isAdmin = "true"; 168 | } 169 | 170 | String username = GetUsernameFromRequest(r); 171 | r->send(200, "application/json", "{\"username\":\"" + username + "\", \"admin\":" + isAdmin + "}"); 172 | } 173 | 174 | //List all users registered in this WebStick 175 | void HandleUserList(AsyncWebServerRequest *r) { 176 | if (!HandleAuth(r)) { 177 | return; 178 | } 179 | 180 | //Build the json with brute force 181 | String jsonString = "["; 182 | //As the DB do not support list, it directly access the root of the folder where the kvdb stores the entries 183 | File root = SD.open(DB_root + "user/"); 184 | bool firstObject = true; 185 | if (root) { 186 | while (true) { 187 | File entry = root.openNextFile(); 188 | if (!entry) { 189 | // No more files 190 | break; 191 | } else { 192 | //There are more lines. Add a , to the end of the previous json object 193 | if (!firstObject) { 194 | jsonString = jsonString + ","; 195 | } else { 196 | firstObject = false; 197 | } 198 | 199 | //Filter out all the directory if any 200 | if (entry.isDirectory()) { 201 | continue; 202 | } 203 | 204 | //Append to the JSON line 205 | jsonString = jsonString + "{\"Username\":\"" + entry.name() + "\"}"; 206 | } 207 | } 208 | } 209 | jsonString += "]"; 210 | 211 | r->send(200, "application/json", jsonString); 212 | 213 | } 214 | -------------------------------------------------------------------------------- /firmware/v3/web-server/web-server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server Stick v3 4 | Author: Toby Chui 5 | 6 | This firmware load and serve web content 7 | from microSD card. 8 | 9 | The following firmware config are recommended 10 | Board: Wemos D1 Mini 11 | CPU clockspeed: 160Mhz 12 | IwIP Varient: v2 Higher Bandwidth 13 | 14 | Require external library: 15 | - ESPAsyncTCP (https://github.com/me-no-dev/ESPAsyncTCP) 16 | - ESPAsyncWebServer (https://github.com/me-no-dev/ESPAsyncWebServer) 17 | - ArduinoJson (https://github.com/bblanchon/ArduinoJson) 18 | - ESPping (https://github.com/dvarrel/ESPping) 19 | - Wake On LAN (https://github.com/a7md0/WakeOnLan) 20 | */ 21 | 22 | //WiFi library 23 | #include 24 | 25 | //SD cards library 26 | #include 27 | #include 28 | #include 29 | 30 | //Web server library 31 | #include 32 | #include 33 | #include 34 | 35 | //Discovery related library 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | 43 | /* Hardware Configurations */ 44 | #define CS_PIN D0 45 | 46 | /* SD Card SPI Speed Definations */ 47 | #define SDSPI_HALF_SPEED 4000000 48 | #define SDSPI_DEFAULT_SPEED 32000000 49 | #define SDSPI_HIGH_SPEED 64000000 50 | 51 | /* Software Global Variables */ 52 | AsyncWebServer server(80); 53 | String adminUsername = ""; 54 | String adminPassword = ""; 55 | String mdnsName = "webstick"; 56 | String authSession = ""; //Session key for admin 57 | int SDCardInitSpeed = SDSPI_HIGH_SPEED; 58 | 59 | /* Time Keeping */ 60 | WiFiUDP ntpUDP; 61 | NTPClient timeClient(ntpUDP, "pool.ntp.org"); 62 | 63 | /* Wake On Lan */ 64 | WakeOnLan WOL(ntpUDP); 65 | 66 | /* Debug variables */ 67 | 68 | /* Function definations */ 69 | String loadWiFiInfoFromSD(); 70 | 71 | 72 | void setup() { 73 | // Setup Debug Serial Port 74 | Serial.begin(115200); 75 | 76 | //Try Initialize SD card (blocking) 77 | while (!SD.begin(CS_PIN, SDCardInitSpeed)) { 78 | if (SDCardInitSpeed == SDSPI_HIGH_SPEED) { 79 | //Fallback to default speed 80 | SDCardInitSpeed = SDSPI_DEFAULT_SPEED; 81 | Serial.println("SD card initialization failed. Falling back to default speed"); 82 | } else if (SDCardInitSpeed == SDSPI_DEFAULT_SPEED) { 83 | //Fallback to half speed (legacy mode) 84 | SDCardInitSpeed = SDSPI_HALF_SPEED; 85 | Serial.println("SD card initialization failed. Falling back to legacy SPI_HALF_SPEED"); 86 | }else{ 87 | Serial.println("SD card initialization failed. Retrying in 3 seconds..."); 88 | } 89 | delay(3000); 90 | } 91 | Serial.println("SD card initialized"); 92 | Serial.println("\n\nStorage Info:"); 93 | Serial.println("----------------------"); 94 | getSDCardTotalSpace(); 95 | getSDCardUsedSpace(); 96 | Serial.println("----------------------"); 97 | Serial.println(); 98 | 99 | //Connect to wifi based on settings (cfg/wifi.txt) 100 | initWiFiConn(); 101 | 102 | //Load admin credentials from SD card (cfg/admin.txt) 103 | initAdminCredentials(); 104 | 105 | //Start mDNS service 106 | initmDNSName(); 107 | if (!MDNS.begin(mdnsName)) { 108 | Serial.println("mDNS Error. Skipping."); 109 | } else { 110 | Serial.println("mDNS started. Connect to your webstick using http://" + mdnsName + ".local"); 111 | MDNS.addService("http", "tcp", 80); 112 | } 113 | 114 | //Start NTP time client 115 | timeClient.begin(); 116 | Serial.print("Requesting time from NTP (unix timestamp): "); 117 | timeClient.update(); 118 | Serial.println(getTime()); 119 | 120 | //Wake on Lan Settings 121 | WOL.setRepeat(3, 100); 122 | WOL.calculateBroadcastAddress(WiFi.localIP(), WiFi.subnetMask()); 123 | 124 | //Initialize database 125 | DBInit(); 126 | 127 | //Resume login session if any 128 | initLoginSessionKey(); 129 | 130 | // Start listening to HTTP Requests 131 | initWebServer(); 132 | } 133 | 134 | 135 | void loop() { 136 | MDNS.update(); 137 | timeClient.update(); 138 | } 139 | -------------------------------------------------------------------------------- /firmware/v3/web-server/web-server.ino.d1_mini.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v3/web-server/web-server.ino.d1_mini.bin -------------------------------------------------------------------------------- /firmware/v3/web-server/web-server.ino.d1_mini.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v3/web-server/web-server.ino.d1_mini.zip -------------------------------------------------------------------------------- /firmware/v4/web-server/kvdb.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Key Value database 4 | 5 | This is a file system based database 6 | that uses foldername as table name, 7 | filename as key and content as value 8 | 9 | Folder name and filename are limited to 10 | 5 characters as SDFS requirements. 11 | */ 12 | 13 | //Root of the db on SD card, **must have tailing slash** 14 | const String DB_root = "/db/"; 15 | 16 | //Clean the input for any input string 17 | String DBCleanInput(const String& inputString) { 18 | String trimmedString = inputString; 19 | //Replae all the slash that might breaks the file system 20 | trimmedString.replace("/", ""); 21 | //Trim off the space before and after the string 22 | trimmedString.trim(); 23 | return trimmedString; 24 | } 25 | 26 | //Database init create all the required table for basic system operations 27 | void DBInit() { 28 | /* Preference persistent store */ 29 | DBNewTable("pref"); //Preference settings 30 | /* User Authentications Tables */ 31 | DBNewTable("auth"); //Auth session store 32 | DBNewTable("user"); //User accounts store 33 | DBNewTable("sess"); //Session store 34 | /* Share System Tables */ 35 | DBNewTable("shln");//Shared links to filename map 36 | DBNewTable("shfn");//Shared filename to links map 37 | } 38 | 39 | //Create a new Database table 40 | void DBNewTable(String tableName) { 41 | tableName = DBCleanInput(tableName); 42 | if (!SD.exists(DB_root + tableName)) { 43 | SD.mkdir(DB_root + tableName); 44 | Serial.println("KVDB table created "+ tableName); 45 | } 46 | } 47 | 48 | 49 | //Check if a database table exists 50 | bool DBTableExists(String tableName) { 51 | tableName = DBCleanInput(tableName); 52 | return SD.exists(DB_root + tableName); 53 | } 54 | 55 | //Write a key to a table, return true if succ 56 | bool DBWrite(String tableName, String key, String value) { 57 | if (!DBTableExists(tableName)) { 58 | Serial.println("KVDB table name "+ tableName + " not exists!"); 59 | return false; 60 | } 61 | tableName = DBCleanInput(tableName); 62 | key = DBCleanInput(key); 63 | String fsDataPath = DB_root + tableName + "/" + key; 64 | if (SD.exists(fsDataPath)) { 65 | //Entry already exists. Delete it 66 | SD.remove(fsDataPath); 67 | } 68 | 69 | //Write new data to it 70 | File targetEntry = SD.open(fsDataPath, FILE_WRITE); 71 | targetEntry.print(value); 72 | targetEntry.close(); 73 | Serial.println("KVDB Entry written to: "+ fsDataPath); 74 | return true; 75 | } 76 | 77 | //Read from database, require table name and key 78 | String DBRead(String tableName, String key) { 79 | if (!DBTableExists(tableName)) { 80 | return ""; 81 | } 82 | tableName = DBCleanInput(tableName); 83 | key = DBCleanInput(key); 84 | String fsDataPath = DB_root + tableName + "/" + key; 85 | if (!SD.exists(fsDataPath)) { 86 | //Target not exists. Return empty string 87 | return ""; 88 | } 89 | 90 | String value = ""; 91 | File targetEntry = SD.open(fsDataPath, FILE_READ); 92 | while (targetEntry.available()) { 93 | value = value + targetEntry.readString(); 94 | } 95 | 96 | targetEntry.close(); 97 | return value; 98 | } 99 | 100 | //Check if a given key exists in the database 101 | bool DBKeyExists(String tableName, String key) { 102 | if (!DBTableExists(tableName)) { 103 | return false; 104 | } 105 | tableName = DBCleanInput(tableName); 106 | key = DBCleanInput(key); 107 | String fsDataPath = DB_root + tableName + "/" + key; 108 | return SD.exists(fsDataPath); 109 | } 110 | 111 | //Remove the key-value item from db, return true if succ 112 | bool DBRemove(String tableName, String key) { 113 | if (!DBKeyExists(tableName, key)){ 114 | return false; 115 | } 116 | tableName = DBCleanInput(tableName); 117 | key = DBCleanInput(key); 118 | String fsDataPath = DB_root + tableName + "/" + key; 119 | SD.remove(fsDataPath); 120 | return true; 121 | } 122 | -------------------------------------------------------------------------------- /firmware/v4/web-server/lib/WakeOnLan.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v4/web-server/lib/WakeOnLan.zip -------------------------------------------------------------------------------- /firmware/v4/web-server/router.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Router.ino 4 | 5 | This is the main router of the whole web server. 6 | It is like the apache.conf where you handle routing 7 | to different services. 8 | 9 | By default, all route will go to the SD card /www/ folder 10 | */ 11 | class MainRouter : public AsyncWebHandler { 12 | public: 13 | MainRouter() {} 14 | virtual ~MainRouter() {} 15 | 16 | bool canHandle(AsyncWebServerRequest *request) { 17 | String requestURI = request->url().c_str(); 18 | if (requestURI.equals("/upload")) { 19 | //File Upload Endpoint 20 | return false; 21 | } else if (requestURI.startsWith("/api/")) { 22 | //API paths 23 | return false; 24 | } else if (requestURI.startsWith("/share/")) { 25 | //Share paths 26 | return false; 27 | } 28 | return true; 29 | } 30 | 31 | //Main Routing Logic Here 32 | void handleRequest(AsyncWebServerRequest *request) { 33 | String requestURI = request->url().c_str(); 34 | 35 | /* Rewrite the request path if URI contains ./ */ 36 | if (requestURI.indexOf("./") > 0) { 37 | requestURI.replace("./", ""); 38 | AsyncWebServerResponse *response = request->beginResponse(307); 39 | response->addHeader("Cache-Control", "no-cache"); 40 | response->addHeader("Location", requestURI); 41 | request->send(response); 42 | return; 43 | } 44 | 45 | /* Special Routing Rules */ 46 | //Redirect / back to index.html 47 | if (requestURI == "/") { 48 | request->redirect("/index.html"); 49 | return; 50 | } 51 | 52 | //Special interfaces that require access controls 53 | if (requestURI.startsWith("/store/")) { 54 | //Private file storage. Not allow access 55 | AsyncWebServerResponse *response = request->beginResponse(401, "text/html", "403 - Forbidden"); 56 | request->send(response); 57 | return; 58 | } 59 | 60 | /* Default Routing Rules */ 61 | Serial.println("URI: " + requestURI + " | MIME: " + getMime(requestURI)); 62 | //Check if the file exists on the SD card 63 | if (SD.exists("/www" + requestURI)) { 64 | // File exists on SD card web root 65 | if (IsDir("/www" + requestURI)) { 66 | //Requesting a directory 67 | if (!requestURI.endsWith("/")) { 68 | //Missing tailing slash 69 | request->redirect(requestURI + "/"); 70 | return; 71 | } 72 | 73 | if (SD.exists("/www" + requestURI + "index.html")) { 74 | request->send(SDFS, "/www" + requestURI + "/index.html", "text/html", false); 75 | } else { 76 | HandleDirRender(request, requestURI, "/www" + requestURI); 77 | } 78 | 79 | } else { 80 | request->send(SDFS, "/www" + requestURI, getMime(requestURI), false); 81 | } 82 | } else { 83 | // File does not exist in web root 84 | AsyncResponseStream *response = request->beginResponseStream("text/html"); 85 | Serial.println("NOT FOUND: " + requestURI); 86 | prettyPrintRequest(request); 87 | response->print("Not Found"); 88 | response->print("

404 - Not Found

"); 89 | response->printf("

Requesting http://%s with URI: %s

", request->host().c_str(), request->url().c_str()); 90 | response->print(""); 91 | request->send(response); 92 | } 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /firmware/v4/web-server/server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server 4 | 5 | This is the main entry point of the WebStick bare metal 6 | web server. If you have exception rules that shall not 7 | be handled by the main router, you can do them here. 8 | 9 | */ 10 | 11 | //Check if a user is authenticated / logged in 12 | bool IsUserAuthed(AsyncWebServerRequest *request) { 13 | if (request->hasHeader("Cookie")) { 14 | //User cookie from browser 15 | String authCookie = GetCookieValueByKey(request, "web-auth"); 16 | if (authCookie == "") { 17 | return false; 18 | } 19 | 20 | //Check if it is user login (no state keeping) 21 | bool isUserLogin = DBKeyExists("sess", authCookie); 22 | if (isUserLogin){ 23 | //User login 24 | return true; 25 | } 26 | 27 | //Check if it is admin login (state keeping) 28 | if (authSession == "") { 29 | //Server side has no resumable login session 30 | return false; 31 | } 32 | 33 | bool isAdminLogin = authCookie.equals(authSession); 34 | if (isAdminLogin) { 35 | //Admin login 36 | return true; 37 | } 38 | 39 | return false; 40 | } else { 41 | Serial.println("Cookie Missing"); 42 | return false; 43 | } 44 | } 45 | 46 | //Check if a user is authenticated and is Admin 47 | bool IsAdmin(AsyncWebServerRequest *request) { 48 | if (request->hasHeader("Cookie")) { 49 | //User cookie from browser 50 | String authCookie = GetCookieValueByKey(request, "web-auth"); 51 | if (authCookie == "") { 52 | return false; 53 | } 54 | 55 | //Match it to the server side value in kvdb 56 | if (authSession == "") { 57 | //Server side has no resumable login session 58 | return false; 59 | } 60 | if (authCookie.equals(authSession)) { 61 | return true; 62 | } 63 | return false; 64 | } else { 65 | return false; 66 | } 67 | } 68 | 69 | 70 | //Reply the request by a directory list 71 | void HandleDirRender(AsyncWebServerRequest *r, String dirName, String dirToList) { 72 | AsyncResponseStream *response = r->beginResponseStream("text/html"); 73 | //Serve directory entries 74 | File directory = SD.open(dirToList); 75 | 76 | // Check if the directory is open 77 | if (!directory) { 78 | SendErrorResp(r, "unable to open directory"); 79 | return; 80 | } 81 | 82 | response->print("Content of " + dirName + ""); 83 | response->print("

Content of " + dirName + "


Back"); 105 | response->print("

"); 106 | r->send(response); 107 | } 108 | 109 | 110 | void initWebServer() { 111 | /* 112 | Other handles here, like this 113 | server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){ 114 | request->send(401); 115 | }); 116 | */ 117 | 118 | /* 119 | server.on("/test", HTTP_GET, [](AsyncWebServerRequest * request) { 120 | getSDCardUsedSpace(); 121 | request->send(200); 122 | }); 123 | */ 124 | 125 | /* Authentication Functions */ 126 | server.on("/api/auth/chk", HTTP_GET, HandleCheckAuth); 127 | server.on("/api/auth/login", HTTP_POST, HandleLogin); 128 | server.on("/api/auth/logout", HTTP_GET, HandleLogout); 129 | 130 | /* User System Functions */ 131 | server.on("/api/user/info", HTTP_GET, HandleGetUserinfo); 132 | server.on("/api/user/new", HTTP_POST, HandleNewUser); 133 | server.on("/api/user/chpw", HTTP_POST, HandleUserChangePassword); 134 | server.on("/api/user/del", HTTP_POST, HandleRemoveUser); 135 | server.on("/api/user/list", HTTP_GET, HandleUserList); 136 | 137 | /* File System Functions */ 138 | server.on("/api/fs/list", HTTP_GET, HandleListDir); 139 | server.on("/api/fs/del", HTTP_POST, HandleFileDel); 140 | server.on("/api/fs/move", HTTP_POST, HandleFileRename); 141 | server.on("/api/fs/download", HTTP_GET, HandleFileDownload); 142 | server.on("/api/fs/newFolder", HTTP_POST, HandleNewFolder); 143 | server.on("/api/fs/disk", HTTP_GET, HandleLoadSpaceInfo); 144 | server.on("/api/fs/properties", HTTP_GET, HandleFileProp); 145 | server.on("/api/fs/search", HTTP_GET, HandleFileSearch); 146 | 147 | /* File Share Functions */ 148 | server.on("/api/share/new", HTTP_POST, HandleCreateShare); 149 | server.on("/api/share/del", HTTP_POST, HandleRemoveShare); 150 | server.on("/api/share/list", HTTP_GET, HandleShareList); 151 | server.on("/api/share/clean", HTTP_GET, HandleShareListCleaning); 152 | server.on("/share", HTTP_GET, HandleShareAccess); 153 | 154 | /* Preference */ 155 | server.on("/api/pref/set", HTTP_GET, HandleSetPref); 156 | server.on("/api/pref/get", HTTP_GET, HandleLoadPref); 157 | 158 | /* Others */ 159 | server.on("/api/info/wifi", HTTP_GET, HandleWiFiInfo); //Show WiFi Information 160 | server.on("/api/wol", HTTP_GET, HandleWakeOnLan); //Handle WoL request 161 | 162 | //File upload handler. see upload.ino 163 | server.onFileUpload(handleFileUpload); 164 | 165 | //Not found handler 166 | server.onNotFound([](AsyncWebServerRequest *request) { 167 | //Generally it will not arrive here as NOT FOUND is also handled in the main router. 168 | //See router.ino for implementation details. 169 | prettyPrintRequest(request); 170 | request->send(404, "text/plain", "Not Found"); 171 | }); 172 | 173 | //Main Router, see router.ino 174 | server.addHandler(new MainRouter()); 175 | 176 | server.begin(); 177 | } 178 | -------------------------------------------------------------------------------- /firmware/v4/web-server/upload.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Upload.ino 4 | 5 | This script handles file upload to the web-stick 6 | by default this function require authentication. 7 | Hence, admin.txt must be set before use 8 | 9 | */ 10 | 11 | void handleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { 12 | // make sure authenticated before allowing upload 13 | if (IsUserAuthed(request)) { 14 | String logmessage = ""; 15 | //String logmessage = "Client:" + request->client()->remoteIP().toString() + " " + request->url(); 16 | //Serial.println(logmessage); 17 | 18 | //Rewrite the filename if it is too long 19 | filename = trimFilename(filename); 20 | 21 | //Get the dir to store the file 22 | String dirToStore = GetPara(request, "dir"); 23 | if (!dirToStore.startsWith("/")) { 24 | dirToStore = "/" + dirToStore; 25 | } 26 | 27 | if (!dirToStore.endsWith("/")) { 28 | dirToStore = dirToStore + "/"; 29 | } 30 | dirToStore = "/www" + dirToStore; 31 | 32 | if (!index) { 33 | Serial.println("Selected Upload Dir: " + dirToStore); 34 | logmessage = "Upload Start: " + String(filename) + " by " + request->client()->remoteIP().toString(); 35 | // open the file on first call and store the file handle in the request object 36 | if (!SD.exists(dirToStore)) { 37 | SD.mkdir(dirToStore); 38 | } 39 | 40 | //Already exists. Overwrite 41 | if (SD.exists(dirToStore + filename)) { 42 | SD.remove(dirToStore + filename); 43 | } 44 | request->_tempFile = SD.open(dirToStore + filename, FILE_WRITE); 45 | Serial.println(logmessage); 46 | } 47 | 48 | if (len) { 49 | // stream the incoming chunk to the opened file 50 | request->_tempFile.write(data, len); 51 | //logmessage = "Writing file: " + String(filename) + " index=" + String(index) + " len=" + String(len); 52 | //Serial.println(logmessage); 53 | } 54 | 55 | if (final) { 56 | logmessage = "Upload Complete: " + String(filename) + ",size: " + String(index + len); 57 | // close the file handle as the upload is now done 58 | request->_tempFile.close(); 59 | Serial.println(logmessage); 60 | 61 | //Check if the file actually exists on SD card 62 | if (!SD.exists(String(dirToStore + filename))) { 63 | //Not found! 64 | SendErrorResp(request, "Write failed for " + String(filename) + ". Try a shorter name!"); 65 | return; 66 | } 67 | request->send(200, "application/json", "ok"); 68 | } 69 | } else { 70 | Serial.println("Auth: Failed"); 71 | SendErrorResp(request, "unauthorized"); 72 | } 73 | } 74 | 75 | /* 76 | Upload File Trimming 77 | 78 | This trim the given uploading filename to less than 32 chars 79 | if the filename is too long to fit on the SD card. 80 | 81 | The code handle UTF-8 trimming at the bytes level. 82 | */ 83 | 84 | //UTF-8 is varaible in length, this get how many bytes in the coming sequences 85 | //are part of this UTF-8 word 86 | uint8_t getUtf8CharLength(const uint8_t firstByte) { 87 | if ((firstByte & 0x80) == 0) { 88 | // Single-byte character 89 | return 1; 90 | } else if ((firstByte & 0xE0) == 0xC0) { 91 | // Two-byte character 92 | return 2; 93 | } else if ((firstByte & 0xF0) == 0xE0) { 94 | // Three-byte character 95 | return 3; 96 | } else if ((firstByte & 0xF8) == 0xF0) { 97 | // Four-byte character 98 | return 4; 99 | } else { 100 | // Invalid UTF-8 character 101 | return 0; 102 | } 103 | } 104 | 105 | String filterBrokenUtf8(const String& input) { 106 | String result; 107 | size_t inputLength = input.length(); 108 | size_t i = 0; 109 | while (i < inputLength) { 110 | uint8_t firstByte = input[i]; 111 | uint8_t charLength = getUtf8CharLength(firstByte); 112 | 113 | if (charLength == 0){ 114 | //End of filter 115 | break; 116 | } 117 | 118 | // Check if the character is complete (non-broken UTF-8) 119 | if (i + charLength <= inputLength) { 120 | // Check for invalid UTF-8 continuation bytes in the character 121 | for (size_t j = 0; j < charLength; j++) { 122 | result += input[i]; 123 | i++; 124 | } 125 | }else{ 126 | //End of valid UTF-8 segment 127 | break; 128 | } 129 | } 130 | return result; 131 | } 132 | 133 | String trimFilename(String& filename) { 134 | //Replace all things that is not suppose to be in the filename 135 | filename.replace("#",""); 136 | filename.replace("?",""); 137 | filename.replace("&",""); 138 | 139 | // Find the position of the last dot (file extension) 140 | int dotIndex = filename.lastIndexOf('.'); 141 | 142 | // Check if the filename contains a dot and the extension is not at the beginning or end 143 | if (dotIndex > 0 && dotIndex < filename.length() - 1) { 144 | // Calculate the maximum length for the filename (excluding extension) 145 | int maxLength = 32 - (filename.length() - dotIndex - 1); 146 | 147 | // Truncate the filename if it's longer than the maximum length 148 | if (filename.length() > maxLength) { 149 | String trimmedFilename = filterBrokenUtf8(filename.substring(0, maxLength)) + filename.substring(dotIndex); 150 | return trimmedFilename; 151 | } 152 | } 153 | 154 | // If no truncation is needed, return the original filename 155 | return filename; 156 | } 157 | -------------------------------------------------------------------------------- /firmware/v4/web-server/users.ino: -------------------------------------------------------------------------------- 1 | /* 2 | User.ino 3 | 4 | This is a new module handling user systems on ESP8266 5 | 6 | */ 7 | 8 | //Check if a user login is valid by username and password 9 | bool UserCheckAuth(String username, String password) { 10 | username.trim(); 11 | password.trim(); 12 | //Load user info from db 13 | if (!DBKeyExists("user", username)) { 14 | return false; 15 | } 16 | 17 | String userHashedPassword = DBRead("user", username); //User hashed password from kvdb 18 | String enteredHashedPassword = sha1(password); //Entered hashed password 19 | return userHashedPassword.equals(enteredHashedPassword); 20 | } 21 | 22 | //Get the username from the request, return empty string if unable to resolve 23 | String GetUsernameFromRequest(AsyncWebServerRequest *r) { 24 | if (r->hasHeader("Cookie")) { 25 | //User cookie from browser 26 | String authCookie = GetCookieValueByKey(r, "web-auth"); 27 | if (authCookie == "") { 28 | return ""; 29 | } 30 | 31 | //Check if this is admin login 32 | if (authCookie.equals(authSession)) { 33 | return adminUsername; 34 | } 35 | 36 | //Check if user login 37 | if (DBKeyExists("sess", authCookie)) { 38 | //Return the username of this session 39 | String username = DBRead("sess", authCookie); 40 | return username; 41 | }else{ 42 | Serial.println("session cookie not found: " + authCookie); 43 | } 44 | 45 | //Not found 46 | return ""; 47 | } 48 | 49 | //This user have no cookie in header 50 | return ""; 51 | } 52 | 53 | //Create new user, creator must be admin 54 | void HandleNewUser(AsyncWebServerRequest *r) { 55 | if (!IsAdmin(r)) { 56 | SendErrorResp(r, "this function require admin permission"); 57 | return; 58 | } 59 | String username = GetPara(r, "username"); 60 | String password = GetPara(r, "password"); 61 | username.trim(); 62 | password.trim(); 63 | 64 | //Check if the inputs are valid 65 | if (username == "" || password == "") { 66 | SendErrorResp(r, "username or password is an empty string"); 67 | return; 68 | } else if (password.length() < 8) { 69 | SendErrorResp(r, "password must contain at least 8 characters"); 70 | return; 71 | } 72 | 73 | //Check if the user already exists 74 | if (DBKeyExists("user", username)) { 75 | SendErrorResp(r, "user with name: " + username + " already exists"); 76 | return; 77 | } 78 | 79 | //OK create the user 80 | bool succ = DBWrite("user", username, sha1(password)); 81 | if (!succ) { 82 | SendErrorResp(r, "write new user to database failed"); 83 | return; 84 | } 85 | r->send(200, "application/json", "\"OK\""); 86 | } 87 | 88 | //Remove the given username from the system 89 | void HandleRemoveUser(AsyncWebServerRequest *r) { 90 | if (!IsAdmin(r)) { 91 | SendErrorResp(r, "this function require admin permission"); 92 | return; 93 | } 94 | 95 | String username = GetPara(r, "username"); 96 | username.trim(); 97 | 98 | //Check if the user exists 99 | if (!DBKeyExists("user", username)) { 100 | SendErrorResp(r, "user with name: " + username + " not exists"); 101 | return; 102 | } 103 | 104 | //Okey, remove the user 105 | bool succ = DBRemove("user", username); 106 | if (!succ) { 107 | SendErrorResp(r, "remove user from system failed"); 108 | return; 109 | } 110 | r->send(200, "application/json", "\"OK\""); 111 | } 112 | 113 | //Admin or the user themselve change password for the account 114 | void HandleUserChangePassword(AsyncWebServerRequest *r) { 115 | //Get requesting username 116 | if (!IsUserAuthed(r)) { 117 | SendErrorResp(r, "user not logged in"); 118 | return; 119 | } 120 | 121 | String currentUser = GetUsernameFromRequest(r); 122 | if (currentUser == "") { 123 | SendErrorResp(r, "unable to load user from system"); 124 | return; 125 | } 126 | 127 | //Check if the user can change password 128 | //note that admin password cannot be changed on-the-fly 129 | //admin password can only be changed in SD card config file 130 | String modifyingUsername = GetPara(r, "username"); 131 | String newPassword = GetPara(r, "newpw"); 132 | modifyingUsername.trim(); 133 | newPassword.trim(); 134 | 135 | if (modifyingUsername == adminUsername) { 136 | SendErrorResp(r, "admin username can only be changed in the config file"); 137 | return; 138 | } 139 | if (currentUser == adminUsername || modifyingUsername == currentUser) { 140 | //Allow modify 141 | if (newPassword.length() < 8) { 142 | SendErrorResp(r, "password must contain at least 8 characters"); 143 | return; 144 | } 145 | 146 | //Write to database 147 | bool succ = DBWrite("user", modifyingUsername, sha1(newPassword)); 148 | if (!succ) { 149 | SendErrorResp(r, "write new user to database failed"); 150 | return; 151 | } 152 | SendOK(r); 153 | 154 | } else { 155 | SendErrorResp(r, "permission denied"); 156 | return; 157 | } 158 | 159 | SendOK(r); 160 | } 161 | 162 | //Get the current username 163 | void HandleGetUserinfo(AsyncWebServerRequest *r){ 164 | if (!HandleAuth(r)) { 165 | return; 166 | } 167 | 168 | String isAdmin = "false"; 169 | if (IsAdmin(r)){ 170 | isAdmin = "true"; 171 | } 172 | 173 | String username = GetUsernameFromRequest(r); 174 | r->send(200, "application/json", "{\"username\":\"" + username + "\", \"admin\":" + isAdmin + "}"); 175 | } 176 | 177 | //List all users registered in this WebStick 178 | void HandleUserList(AsyncWebServerRequest *r) { 179 | if (!HandleAuth(r)) { 180 | return; 181 | } 182 | 183 | //Build the json with brute force 184 | String jsonString = "["; 185 | //As the DB do not support list, it directly access the root of the folder where the kvdb stores the entries 186 | File root = SD.open(DB_root + "user/"); 187 | bool firstObject = true; 188 | if (root) { 189 | while (true) { 190 | File entry = root.openNextFile(); 191 | if (!entry) { 192 | // No more files 193 | break; 194 | } else { 195 | //There are more lines. Add a , to the end of the previous json object 196 | if (!firstObject) { 197 | jsonString = jsonString + ","; 198 | } else { 199 | firstObject = false; 200 | } 201 | 202 | //Filter out all the directory if any 203 | if (entry.isDirectory()) { 204 | continue; 205 | } 206 | 207 | //Append to the JSON line 208 | jsonString = jsonString + "{\"Username\":\"" + entry.name() + "\"}"; 209 | } 210 | } 211 | } 212 | jsonString += "]"; 213 | 214 | r->send(200, "application/json", jsonString); 215 | 216 | } 217 | -------------------------------------------------------------------------------- /firmware/v4/web-server/web-server.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Web Server Stick v3 4 | Author: Toby Chui 5 | 6 | This firmware load and serve web content 7 | from microSD card. 8 | 9 | The following firmware config are recommended 10 | Board: Wemos D1 Mini 11 | CPU clockspeed: 160Mhz 12 | IwIP Varient: v2 Higher Bandwidth 13 | 14 | Require external library: 15 | - ESPAsyncTCP (https://github.com/me-no-dev/ESPAsyncTCP) 16 | - ESPAsyncWebServer (https://github.com/me-no-dev/ESPAsyncWebServer) 17 | - ArduinoJson (https://github.com/bblanchon/ArduinoJson) 18 | - ESPping (https://github.com/dvarrel/ESPping) 19 | - Wake On LAN (https://github.com/a7md0/WakeOnLan) 20 | */ 21 | 22 | //WiFi library 23 | #include 24 | 25 | //SD cards library 26 | #include 27 | #include 28 | #include 29 | 30 | //Web server library 31 | #include 32 | #include 33 | #include 34 | 35 | //Discovery related library 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | 43 | /* Hardware Configurations */ 44 | #define CS_PIN D0 45 | 46 | /* SD Card SPI Speed Definations */ 47 | #define SDSPI_HALF_SPEED 4000000 48 | #define SDSPI_DEFAULT_SPEED 32000000 49 | #define SDSPI_HIGH_SPEED 64000000 50 | 51 | /* Software Global Variables */ 52 | AsyncWebServer server(80); 53 | String adminUsername = ""; 54 | String adminPassword = ""; 55 | String mdnsName = "webstick"; 56 | String authSession = ""; //Session key for admin 57 | int SDCardInitSpeed = SDSPI_HIGH_SPEED; 58 | 59 | /* Time Keeping */ 60 | WiFiUDP ntpUDP; 61 | NTPClient timeClient(ntpUDP, "pool.ntp.org"); 62 | 63 | /* Wake On Lan */ 64 | WakeOnLan WOL(ntpUDP); 65 | 66 | /* Debug variables */ 67 | 68 | /* Function definations */ 69 | String loadWiFiInfoFromSD(); 70 | 71 | 72 | void setup() { 73 | // Setup Debug Serial Port 74 | Serial.begin(115200); 75 | 76 | //Try Initialize SD card (blocking) 77 | while (!SD.begin(CS_PIN, SDCardInitSpeed)) { 78 | if (SDCardInitSpeed == SDSPI_HIGH_SPEED) { 79 | //Fallback to default speed 80 | SDCardInitSpeed = SDSPI_DEFAULT_SPEED; 81 | Serial.println("SD card initialization failed. Falling back to default speed"); 82 | } else if (SDCardInitSpeed == SDSPI_DEFAULT_SPEED) { 83 | //Fallback to half speed (legacy mode) 84 | SDCardInitSpeed = SDSPI_HALF_SPEED; 85 | Serial.println("SD card initialization failed. Falling back to legacy SPI_HALF_SPEED"); 86 | }else{ 87 | Serial.println("SD card initialization failed. Retrying in 3 seconds..."); 88 | } 89 | delay(3000); 90 | } 91 | Serial.println("SD card initialized"); 92 | Serial.println("\n\nStorage Info:"); 93 | Serial.println("----------------------"); 94 | getSDCardTotalSpace(); 95 | getSDCardUsedSpace(); 96 | Serial.println("----------------------"); 97 | Serial.println(); 98 | 99 | //Connect to wifi based on settings (cfg/wifi.txt) 100 | initWiFiConn(); 101 | 102 | //Load admin credentials from SD card (cfg/admin.txt) 103 | initAdminCredentials(); 104 | 105 | //Start mDNS service 106 | initmDNSName(); 107 | if (!MDNS.begin(mdnsName)) { 108 | Serial.println("mDNS Error. Skipping."); 109 | } else { 110 | Serial.println("mDNS started. Connect to your webstick using http://" + mdnsName + ".local"); 111 | MDNS.addService("http", "tcp", 80); 112 | } 113 | 114 | //Start NTP time client 115 | timeClient.begin(); 116 | Serial.print("Requesting time from NTP (unix timestamp): "); 117 | timeClient.update(); 118 | Serial.println(getTime()); 119 | 120 | //Wake on Lan Settings 121 | WOL.setRepeat(3, 100); 122 | WOL.calculateBroadcastAddress(WiFi.localIP(), WiFi.subnetMask()); 123 | 124 | //Initialize database 125 | DBInit(); 126 | 127 | //Resume login session if any 128 | initLoginSessionKey(); 129 | 130 | // Start listening to HTTP Requests 131 | initWebServer(); 132 | } 133 | 134 | 135 | void loop() { 136 | MDNS.update(); 137 | timeClient.update(); 138 | } 139 | -------------------------------------------------------------------------------- /firmware/v4/web-server/web-server.ino.d1_mini.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v4/web-server/web-server.ino.d1_mini.bin -------------------------------------------------------------------------------- /firmware/v4/web-server/web-server.ino.d1_mini.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/firmware/v4/web-server/web-server.ino.d1_mini.zip -------------------------------------------------------------------------------- /img/README/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/1.jpg -------------------------------------------------------------------------------- /img/README/image-20230814160329296.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814160329296.png -------------------------------------------------------------------------------- /img/README/image-20230814160331808.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814160331808.png -------------------------------------------------------------------------------- /img/README/image-20230814160505939.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814160505939.png -------------------------------------------------------------------------------- /img/README/image-20230814160534701.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814160534701.png -------------------------------------------------------------------------------- /img/README/image-20230814160904725.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814160904725.png -------------------------------------------------------------------------------- /img/README/image-20230814161008552.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814161008552.png -------------------------------------------------------------------------------- /img/README/image-20230814161027242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814161027242.png -------------------------------------------------------------------------------- /img/README/image-20230814161040075.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814161040075.png -------------------------------------------------------------------------------- /img/README/image-20230814161104181.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20230814161104181.png -------------------------------------------------------------------------------- /img/README/image-20240323120647781.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120647781.png -------------------------------------------------------------------------------- /img/README/image-20240323120841577.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120841577.png -------------------------------------------------------------------------------- /img/README/image-20240323120851503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120851503.png -------------------------------------------------------------------------------- /img/README/image-20240323120859631.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120859631.png -------------------------------------------------------------------------------- /img/README/image-20240323120923170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120923170.png -------------------------------------------------------------------------------- /img/README/image-20240323120934323.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323120934323.png -------------------------------------------------------------------------------- /img/README/image-20240323121044080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323121044080.png -------------------------------------------------------------------------------- /img/README/image-20240323122057879.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20240323122057879.png -------------------------------------------------------------------------------- /img/README/image-20250510203307491.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203307491.png -------------------------------------------------------------------------------- /img/README/image-20250510203347375.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203347375.png -------------------------------------------------------------------------------- /img/README/image-20250510203411915.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203411915.png -------------------------------------------------------------------------------- /img/README/image-20250510203428665.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203428665.png -------------------------------------------------------------------------------- /img/README/image-20250510203529903.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203529903.png -------------------------------------------------------------------------------- /img/README/image-20250510203605617.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203605617.png -------------------------------------------------------------------------------- /img/README/image-20250510203636902.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203636902.png -------------------------------------------------------------------------------- /img/README/image-20250510203736747.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/img/README/image-20250510203736747.png -------------------------------------------------------------------------------- /pcb/v2/BOM_webstick v2_2023-08-18.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v2/BOM_webstick v2_2023-08-18.csv -------------------------------------------------------------------------------- /pcb/v2/Gerber_webstick v2.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v2/Gerber_webstick v2.1.zip -------------------------------------------------------------------------------- /pcb/v2/Gerber_webstick v2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v2/Gerber_webstick v2.zip -------------------------------------------------------------------------------- /pcb/v2/PickAndPlace_webstick v2_2023-08-18.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v2/PickAndPlace_webstick v2_2023-08-18.csv -------------------------------------------------------------------------------- /pcb/v3/BOM_Website-Stick_2024-03-20.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v3/BOM_Website-Stick_2024-03-20.csv -------------------------------------------------------------------------------- /pcb/v3/Gerber_Website-Stick_webstick-v3_2024-03-20.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v3/Gerber_Website-Stick_webstick-v3_2024-03-20.zip -------------------------------------------------------------------------------- /pcb/v3/PickAndPlace_webstick-v3_2024-03-20.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v3/PickAndPlace_webstick-v3_2024-03-20.csv -------------------------------------------------------------------------------- /pcb/v4/BOM_Website-Stick_2025-02-28.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v4/BOM_Website-Stick_2025-02-28.csv -------------------------------------------------------------------------------- /pcb/v4/Gerber_Website-Stick_webstick-v4_2025-02-28.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v4/Gerber_Website-Stick_webstick-v4_2025-02-28.zip -------------------------------------------------------------------------------- /pcb/v4/PickAndPlace_webstick-v4_2025-02-28.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/pcb/v4/PickAndPlace_webstick-v4_2025-02-28.csv -------------------------------------------------------------------------------- /res/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/res/favicon.png -------------------------------------------------------------------------------- /res/favicon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/res/favicon.psd -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/res/logo.png -------------------------------------------------------------------------------- /res/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/res/logo.psd -------------------------------------------------------------------------------- /sd_card/cfg/admin.txt: -------------------------------------------------------------------------------- 1 | admin 2 | admin -------------------------------------------------------------------------------- /sd_card/cfg/mdns.txt: -------------------------------------------------------------------------------- 1 | webstick -------------------------------------------------------------------------------- /sd_card/cfg/wifi.txt: -------------------------------------------------------------------------------- 1 | WIFI_SSID 2 | WiFiPasswordHere -------------------------------------------------------------------------------- /sd_card/www/about.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 |
6 |
What is a WebStick?
7 | WebStick is a USB stick form factor web server powered by a ESP12E/F module. 8 | The web content is serve over the SD card connected to the MCU via SPI link, 9 | providing basic HTTP web server function with less than 2W of power usage. 10 |
11 | 12 |
Why WebStick?
13 | 14 | If you are just hosting a web server for personal use with a few occasional guests, 15 | you do not need a Raspberry Pi that draws 15W 24/7 just for this purpose. 16 | 17 | Instead, a WebStick with ESP8266 is just enough to do the tasks. 18 | With the help of CDN and Caching, even a 2W MCU running over WiFi 19 | can provide relatively acceptable performance for basic HTTP web services. 20 |
21 | 22 |
Specification
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
ItemProperties
Power Requirement5V 1A
Actual Power Drawaround 2 - 3W
Wireless Network Speed2 - 4 Mbps
Max SD card size4GB
SD card formatFAT32
Arduino IDE Support
Run PHP / nodejs
Max No. of Users~20 viewers per day
63 |
-------------------------------------------------------------------------------- /sd_card/www/admin/img/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/cluster.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/eq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/opr/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/opr/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/opr/new_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/opr/open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/opr/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/wifi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/zoom-in.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sd_card/www/admin/img/zoom-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sd_card/www/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin Panel 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 46 |
47 |
48 | 49 |
50 | 53 | 56 | 59 | 62 | 65 |
66 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/all.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

All Posts

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 |
Post NameCreation TimeEditDelete
17 |
18 |
23 | 26 |
27 | 28 | 47 | 48 | 51 |
52 | 53 | 151 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/library.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Image Library

4 |

The table below shows the screenshots pasted into the editor

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 |
Image NameUpload TimePreviewDelete
17 |
18 | Loading... 19 |
23 | 26 |
27 | 28 |

Image Preview

29 |

Click on the preview button to see the image

30 |
31 | 32 |
33 |

34 |
35 | 36 | 109 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/newpg.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 |

Create New Page

11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | .html 22 |
23 |
24 | The page will be saved to /site/pages/ 25 |
26 |
27 |
28 |
29 | 143 | 144 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/pages.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

All Pages

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 |
Page NameEditDelete
16 |
17 |
22 | 25 |
26 | 27 | 46 | 47 | 50 |
51 | 52 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/paste.html: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 |

Paste an image here (Ctrl + V)

12 | Pasted Image 13 |
14 |

No pasted image

15 | 18 |
19 |
20 | 21 | 138 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/pedit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 144 | 145 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/setting.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Site Settings

4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 | 42 |
43 |
44 | 116 | -------------------------------------------------------------------------------- /sd_card/www/admin/posteng/src/en.js: -------------------------------------------------------------------------------- 1 | /* 2 | * wysiwyg web editor 3 | * 4 | * suneditor.js 5 | * Copyright 2017 JiHong Lee. 6 | * MIT license. 7 | */ 8 | 'use strict'; 9 | 10 | (function (global, factory) { 11 | if (typeof module === 'object' && typeof module.exports === 'object') { 12 | module.exports = global.document ? 13 | factory(global, true) : 14 | function (w) { 15 | if (!w.document) { 16 | throw new Error('SUNEDITOR_LANG a window with a document'); 17 | } 18 | return factory(w); 19 | }; 20 | } else { 21 | factory(global); 22 | } 23 | }(typeof window !== 'undefined' ? window : this, function (window, noGlobal) { 24 | const lang = { 25 | code: 'en', 26 | toolbar: { 27 | default: 'Default', 28 | save: 'Save', 29 | font: 'Font', 30 | formats: 'Formats', 31 | fontSize: 'Size', 32 | bold: 'Bold', 33 | underline: 'Underline', 34 | italic: 'Italic', 35 | strike: 'Strike', 36 | subscript: 'Subscript', 37 | superscript: 'Superscript', 38 | removeFormat: 'Remove Format', 39 | fontColor: 'Font Color', 40 | hiliteColor: 'Highlight Color', 41 | indent: 'Indent', 42 | outdent: 'Outdent', 43 | align: 'Align', 44 | alignLeft: 'Align left', 45 | alignRight: 'Align right', 46 | alignCenter: 'Align center', 47 | alignJustify: 'Align justify', 48 | list: 'List', 49 | orderList: 'Ordered list', 50 | unorderList: 'Unordered list', 51 | horizontalRule: 'Horizontal line', 52 | hr_solid: 'Solid', 53 | hr_dotted: 'Dotted', 54 | hr_dashed: 'Dashed', 55 | table: 'Table', 56 | link: 'Link', 57 | math: 'Math', 58 | image: 'Image', 59 | video: 'Video', 60 | audio: 'Audio', 61 | fullScreen: 'Full screen', 62 | showBlocks: 'Show blocks', 63 | codeView: 'Code view', 64 | undo: 'Undo', 65 | redo: 'Redo', 66 | preview: 'Preview', 67 | print: 'print', 68 | tag_p: 'Paragraph', 69 | tag_div: 'Normal (DIV)', 70 | tag_h: 'Header', 71 | tag_blockquote: 'Quote', 72 | tag_pre: 'Code', 73 | template: 'Template', 74 | lineHeight: 'Line height', 75 | paragraphStyle: 'Paragraph style', 76 | textStyle: 'Text style', 77 | imageGallery: 'Image gallery', 78 | mention: 'Mention' 79 | }, 80 | dialogBox: { 81 | linkBox: { 82 | title: 'Insert Link', 83 | url: 'URL to link', 84 | text: 'Text to display', 85 | newWindowCheck: 'Open in new window', 86 | downloadLinkCheck: 'Download link', 87 | bookmark: 'Bookmark' 88 | }, 89 | mathBox: { 90 | title: 'Math', 91 | inputLabel: 'Mathematical Notation', 92 | fontSizeLabel: 'Font Size', 93 | previewLabel: 'Preview' 94 | }, 95 | imageBox: { 96 | title: 'Insert image', 97 | file: 'Select from files', 98 | url: 'Image URL', 99 | altText: 'Alternative text' 100 | }, 101 | videoBox: { 102 | title: 'Insert Video', 103 | file: 'Select from files', 104 | url: 'Media embed URL, YouTube/Vimeo' 105 | }, 106 | audioBox: { 107 | title: 'Insert Audio', 108 | file: 'Select from files', 109 | url: 'Audio URL' 110 | }, 111 | browser: { 112 | tags: 'Tags', 113 | search: 'Search', 114 | }, 115 | caption: 'Insert description', 116 | close: 'Close', 117 | submitButton: 'Submit', 118 | revertButton: 'Revert', 119 | proportion: 'Constrain proportions', 120 | basic: 'Basic', 121 | left: 'Left', 122 | right: 'Right', 123 | center: 'Center', 124 | width: 'Width', 125 | height: 'Height', 126 | size: 'Size', 127 | ratio: 'Ratio' 128 | }, 129 | controller: { 130 | edit: 'Edit', 131 | unlink: 'Unlink', 132 | remove: 'Remove', 133 | insertRowAbove: 'Insert row above', 134 | insertRowBelow: 'Insert row below', 135 | deleteRow: 'Delete row', 136 | insertColumnBefore: 'Insert column before', 137 | insertColumnAfter: 'Insert column after', 138 | deleteColumn: 'Delete column', 139 | fixedColumnWidth: 'Fixed column width', 140 | resize100: 'Resize 100%', 141 | resize75: 'Resize 75%', 142 | resize50: 'Resize 50%', 143 | resize25: 'Resize 25%', 144 | autoSize: 'Auto size', 145 | mirrorHorizontal: 'Mirror, Horizontal', 146 | mirrorVertical: 'Mirror, Vertical', 147 | rotateLeft: 'Rotate left', 148 | rotateRight: 'Rotate right', 149 | maxSize: 'Max size', 150 | minSize: 'Min size', 151 | tableHeader: 'Table header', 152 | mergeCells: 'Merge cells', 153 | splitCells: 'Split Cells', 154 | HorizontalSplit: 'Horizontal split', 155 | VerticalSplit: 'Vertical split' 156 | }, 157 | menu: { 158 | spaced: 'Spaced', 159 | bordered: 'Bordered', 160 | neon: 'Neon', 161 | translucent: 'Translucent', 162 | shadow: 'Shadow', 163 | code: 'Code' 164 | } 165 | }; 166 | 167 | if (typeof noGlobal === typeof undefined) { 168 | if (!window.SUNEDITOR_LANG) { 169 | Object.defineProperty(window, 'SUNEDITOR_LANG', { 170 | enumerable: true, 171 | writable: false, 172 | configurable: false, 173 | value: {} 174 | }); 175 | } 176 | 177 | Object.defineProperty(window.SUNEDITOR_LANG, 'en', { 178 | enumerable: true, 179 | writable: true, 180 | configurable: true, 181 | value: lang 182 | }); 183 | } 184 | 185 | return lang; 186 | })); -------------------------------------------------------------------------------- /sd_card/www/admin/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Search Engine 6 | 7 | 8 | 9 | 10 | 11 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |

46 |
47 |
48 |
49 |

50 | 51 | 52 | 53 | 54 |
55 | File Search 56 |
This function will recursively search the whole SD card using keyword matching algorithm.
57 | This might take a few minutes if you have a lots of files stored on your SD card.
58 |
59 |

60 |

61 |
62 |
63 |


64 | 76 |
77 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /sd_card/www/admin/share.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | File Share | WebStick 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 58 | 59 | 60 |
61 |
62 |
63 |

64 | Loading 65 |
66 |

67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
File Size
Share ID
79 | Download 80 |
81 |
82 |
83 |
84 |
85 |

Download on another device

86 |
87 |
88 |
89 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /sd_card/www/admin/shares.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Share Manager 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |
20 |
21 |

22 | 23 |
24 | Shares Manager 25 |
All the shared files on the SD card
26 |
27 |

28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 |
31 | List of shares on this WebStick 32 |
38 | Loading 39 |
43 | 44 |
45 | Share Entry Cleaning 46 |

Some share links might point to files that no longer exists on the SD card.
47 | Click the "Clean Shares" button below to remove broken share links and regenerate the shared link table.

48 | 49 |
50 |
51 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /sd_card/www/admin/upld.js: -------------------------------------------------------------------------------- 1 | /* 2 | Upld.js 3 | 4 | This script can be included in any other pages 5 | to provide file upload functions 6 | 7 | Usage: handleFile(file, dir, callback) 8 | E.g. 9 | const blob = new Blob(["Hello World!"], { type: 'text/plain' }); 10 | const file = new File([blob], "hello.txt"); 11 | handleFile(file, "/down/", function(){ 12 | alert("Upload done"); 13 | }); 14 | */ 15 | function handleFile(file, dir, callback=undefined) { 16 | // Perform actions with the selected file 17 | console.log('Uploading file:', file); 18 | var formdata = new FormData(); 19 | formdata.append("file1", file); 20 | var ajax = new XMLHttpRequest(); 21 | ajax.upload.addEventListener("progress", progressHandler, false); 22 | ajax.addEventListener("load", function(event){ 23 | completeHandler(event); 24 | if (callback != undefined){ 25 | callback(); 26 | } 27 | }, false); // doesnt appear to ever get called even upon success 28 | ajax.addEventListener("error", errorHandler, false); 29 | ajax.addEventListener("abort", abortHandler, false); 30 | ajax.open("POST", "/upload?dir=" + dir); 31 | ajax.send(formdata); 32 | } 33 | 34 | function progressHandler(event) { 35 | //_("loaded_n_total").innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total; // event.total doesnt show accurate total file size 36 | 37 | var percent = (event.loaded / event.total) * 100; 38 | $("#uploadProgressBar").find(".bar").css("width", Math.round(percent) + "%"); 39 | console.log("Uploaded " + event.loaded + " bytes => " + percent +"%"); 40 | if (percent >= 100) { 41 | $("#uploadProgressBar").find(".bar").css("width", "100%"); 42 | //_("status").innerHTML = "Please wait, writing file to filesystem"; 43 | } 44 | } 45 | function completeHandler(event) { 46 | } 47 | 48 | 49 | function errorHandler(event) { 50 | } 51 | 52 | function abortHandler(event) { 53 | 54 | } -------------------------------------------------------------------------------- /sd_card/www/admin/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 |
28 | 105 | 106 | -------------------------------------------------------------------------------- /sd_card/www/down/Rise Above.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/down/Rise Above.mp3 -------------------------------------------------------------------------------- /sd_card/www/down/minecraft.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/down/minecraft.webm -------------------------------------------------------------------------------- /sd_card/www/down/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/down/photo.jpg -------------------------------------------------------------------------------- /sd_card/www/down/text.txt: -------------------------------------------------------------------------------- 1 | HELLO WORLD! 2 | This is a testing document for download -------------------------------------------------------------------------------- /sd_card/www/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/favicon.png -------------------------------------------------------------------------------- /sd_card/www/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/img/logo.png -------------------------------------------------------------------------------- /sd_card/www/img/selfie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/img/selfie.jpg -------------------------------------------------------------------------------- /sd_card/www/img/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobychui/WebStick/6ab2d26b80b8775c7e0777874bdad3f2e4764aab/sd_card/www/img/wallpaper.jpg -------------------------------------------------------------------------------- /sd_card/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Homepage | WebStick 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 |
42 |
43 | Home 44 | About 45 | Posts 46 | QR 47 |
48 |
49 |
50 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 | 76 |
77 |
78 |

This site is hosted on a WebStick designed by tobychui

79 |
80 |
81 | 82 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /sd_card/www/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
Username
27 |
28 | 29 | 30 |
31 |
Password
32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 | 41 | Back 42 | 43 |
44 |
45 |
46 |
47 | 48 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /sd_card/www/posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
7 |
8 |
Recent Posts
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /sd_card/www/qr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QR Code Generator 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 44 | 45 | 46 |
47 |
48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /sd_card/www/site/img/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Redirecitng to homepage

8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/site/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Redirecitng to homepage

8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/site/posts/1710676940_Hello World!.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | Welcome to my web page. Fun facts: This is hosted on an ESP8266! 3 | 4 | ## WebSticks 5 | The WebStick is an Arduino programmed, mini web server stick power in a USB drive form factor. 6 | 7 | You might be wonder how it is possible to work right? Well, it is not simple. The make it work, I first 8 | need to write a web server in C++ and make it serve html files from an SD card formated as FAT. 9 | Next, I need to handle all kind of headers and werid things to make it happens. 10 | -------------------------------------------------------------------------------- /sd_card/www/site/posts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Redirecitng to homepage

8 | 9 | -------------------------------------------------------------------------------- /sd_card/www/store/README.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | The store folder is a private storage folder which only allow admin access. You can upload files to this folder and use it as a tiny cloud storage. Due to the limitation on hardware, we do not recommend uploading file >5MB as this will take decades to upload and stream. 4 | --------------------------------------------------------------------------------