#include #include #include #include #include #include #include #include // Global Variables // LED pin const int ledPin = LED_BUILTIN; bool ledState = false; // Access Point settings const char* serverName = "r04m1ng.l1br4ry"; // DNS name for captive portal //const char* AP_SSID = "PR0J3K7_B00KM4RK"; // Name of the WiFi network const char* AP_SSID_BASE = "PR0J3K7_B00KM4RK_"; // Base name for SSID String AP_SSID; // Full name with number IPAddress apIP(192, 168, 4, 1); // IP address of the NodeMCU in AP mode const byte DNS_PORT = 53; // DNS server port // Create web server object ESP8266WebServer server(80); DNSServer dnsServer; // SD card CS pin const int SD_CS_PIN = D8; // File Uploads File uploadFile; // Forum cleanup settings unsigned long lastCleanupTime = 0; const unsigned long CLEANUP_INTERVAL = 3600000; // 1 hour in milliseconds // Forum structures struct ForumPost { String id; String author; String content; String timestamp; }; struct ForumThread { String id; String title; String author; String timestamp; }; // Function declarations void handleRoot(); void handleToggle(); void handleFileList(); void handleFileDownload(); void handleNotFound(); void handleCaptivePortal(); void handleForum(); void handleNewThread(); void handleThread(); void handleNewPost(); void checkAndCleanupForum(); void cleanupForum(); void removeDirectory(const char * path); // Function to check if file is allowed bool isAllowedFile(const String& filename) { String lowerFilename = filename; lowerFilename.toLowerCase(); return lowerFilename.endsWith(".pdf") || lowerFilename.endsWith(".epub") || lowerFilename.endsWith(".doc") || lowerFilename.endsWith(".rtf") || lowerFilename.endsWith(".txt"); } // Function to check if string is IP address bool isIp(String str) { for (size_t i = 0; i < str.length(); i++) { int c = str.charAt(i); if (c != '.' && (c < '0' || c > '9')) { return false; } } return true; } String toStringIp(IPAddress ip) { String res = ""; for (int i = 0; i < 3; i++) { res += String((ip >> (8 * i)) & 0xFF) + "."; } res += String(((ip >> 8 * 3)) & 0xFF); return res; } bool captivePortal() { if (!isIp(server.hostHeader())) { server.sendHeader("Location", String("http://") + toStringIp(apIP), true); server.send(302, "text/plain", ""); return true; } return false; } void setup() { // Start Serial for debugging Serial.begin(115200); delay(100); Serial.println("\nStarting setup..."); // Initialize LED pin pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); // Initialize SD card Serial.print("Initializing SD card..."); if (!SD.begin(SD_CS_PIN)) { Serial.println("SD card initialization failed!"); } else { Serial.println("SD card initialization done."); // Clear and reinitialize forum on boot Serial.println("Clearing forum data..."); if (SD.exists("/forum")) { removeDirectory("/forum"); } // Create fresh forum structure SD.mkdir("/forum"); SD.mkdir("/forum/posts"); File threadsFile = SD.open("/forum/threads.json", FILE_WRITE); if (threadsFile) { threadsFile.println("[]"); threadsFile.close(); Serial.println("Forum reinitialized successfully"); } } // Set up Access Point randomSeed(analogRead(0)); // Initialize random seed int randomNum = random(0, 100); // Generate random number 0-99 AP_SSID = String(AP_SSID_BASE) + String(randomNum < 10 ? "0" : "") + String(randomNum); Serial.println("Generated SSID: " + AP_SSID); WiFi.mode(WIFI_AP); WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); // Start AP without password bool apStartSuccess = WiFi.softAP(AP_SSID.c_str(), NULL, 1); Serial.println(apStartSuccess ? "AP Start Success" : "AP Start Failed!"); if (apStartSuccess) { Serial.println("Access Point Started Successfully"); Serial.printf("SSID: %s\n", AP_SSID.c_str()); Serial.printf("AP IP address: %s\n", WiFi.softAPIP().toString().c_str()); } // Configure DNS server to redirect all requests dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(DNS_PORT, "*", apIP); // Set up server routes server.on("/", handleRoot); //Main library page server.on("/toggle", handleToggle); server.on("/list", handleFileList); server.on("/download", handleFileDownload); server.on("/forum", handleForum); server.on("/forum/new", handleNewThread); server.on("/forum/thread", handleThread); server.on("/forum/post", handleNewPost); server.on("/thread", HTTP_GET, handleThreadAjax); server.on("/upload", HTTP_POST, handleUpload, handleFileUpload); server.on("/uploadpage", handleUploadPage);server.on("/", handleRoot); // Main library page // server.on("/generate_204", handleCaptivePortal); // Android //server.on("/gen_204", handleCaptivePortal); // Android //server.on("/ncsi.txt", handleCaptivePortal); // Windows //server.on("/check_network_status.txt", handleCaptivePortal); // Windows //server.on("/hotspot-detect.html", handleCaptivePortal); // iOS //server.on("/success.txt", handleCaptivePortal); // iOS //server.on("/connecttest.txt", handleCaptivePortal); // Windows //server.onNotFound(handleNotFound); // All other requests // Routes for Captive Portal detection server.on("/", handlePortal); server.on("/fwlink", handlePortal); server.on("/generate_204", handlePortal); server.on("/hotspot-detect.html", handlePortal); server.onNotFound(handlePortal); // Add handlers for different captive portal detection server.on("/", []() { if (captivePortal()) { return; } handlePortal(); }); server.onNotFound([]() { if (captivePortal()) { return; } handlePortal(); }); server.begin(); Serial.println("HTTP server started"); // Initialize cleanup timer lastCleanupTime = millis(); } void loop() { dnsServer.processNextRequest(); // DNS server.handleClient(); //HTTP checkAndCleanupForum(); } void checkAndCleanupForum() { unsigned long currentTime = millis(); if ((currentTime - lastCleanupTime >= CLEANUP_INTERVAL) || (currentTime < lastCleanupTime)) { cleanupForum(); lastCleanupTime = currentTime; } } void cleanupForum() { if (SD.exists("/forum")) { removeDirectory("/forum"); } SD.mkdir("/forum"); SD.mkdir("/forum/posts"); File threadsFile = SD.open("/forum/threads.json", FILE_WRITE); if (threadsFile) { threadsFile.println("[]"); threadsFile.close(); } File logFile = SD.open("/forum/cleanup.log", FILE_WRITE); if (logFile) { logFile.printf("Forum cleaned at: %lu\n", millis()); logFile.close(); } } void removeDirectory(const char * path) { File dir = SD.open(path); if (!dir.isDirectory()) { return; } while (true) { File entry = dir.openNextFile(); if (!entry) { break; } String entryPath = String(path) + "/" + entry.name(); if (entry.isDirectory()) { entry.close(); removeDirectory(entryPath.c_str()); } else { entry.close(); SD.remove(entryPath.c_str()); } } dir.close(); SD.rmdir(path); } void handleCaptivePortal() { String html = ""; html += ""; html += ""; html += ""; html += ""; html += "
"; html += "

PR0J3KT B00KM4RK

"; html += "Enter Library"; html += "
"; html += ""; server.send(200, "text/html", html); } void handlePortal() { String html = ""; html += ""; html += ""; html += ""; html += "
"; html += "

PR0J3KT B00KM4RK

"; html += "TO ENTER LIBRARY"; html += "
"; html += "
"; html += "

Accept Sign-In (may vary by device). Open browser and navigate to 192.168.4.1

"; html += "
"; html += ""; server.send(200, "text/html", html); } void handleNotFound() { // If request is not for our IP, redirect to captive portal if (!isIp(server.hostHeader())) { server.sendHeader("Location", String("http://") + toStringIp(apIP), true); server.send(302, "text/plain", ""); return; } // Otherwise, send captive portal page handleCaptivePortal(); } void handleRoot() { // Add headers for captive portal server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); String html = ""; html += ""; html += ""; html += "
"; html += "
7H3 R04M1NG L1BR4RY"; html += "
"; html += "
"; // Add ASCII Owl html += "
";
    html += "      ,___,\n";
    html += "     (O,O)\n";
    html += "     (  v  )\n";
    html += "    -==*^*==-\n";
    html += "
"; html += "
"; html += "
"; html += "

[ Network: " + String(AP_SSID) + " ]
"; // html += "[ IP: " + WiFi.softAPIP().toString() + " ]
"; html += "[ Connected Users: " + String(WiFi.softAPgetStationNum()) + " ]
"; html += "[ System Status: " + String(ledState ? "ACTIVE" : "STANDBY") + " ]

"; html += "
"; html += "
"; html += "

// P057-2-F0RUM //

"; html += "
"; html += "
"; html += "
"; html += "

// L34V3-4-F1L3 //

"; html += "
"; html += "
"; html += "
"; html += "

// 74k3-4-F1L3 //

"; html += "
"; html += "
"; server.send(200, "text/html", html); } void handleForum() { String html = ""; html += ""; html += ""; html += "

// TERMINAL FORUM //

"; // Display cleanup timer html += "
"; html += "[NEXT RESET IN: "; unsigned long timeLeft = CLEANUP_INTERVAL - (millis() - lastCleanupTime); int hoursLeft = timeLeft / 3600000; int minutesLeft = (timeLeft % 3600000) / 60000; int secondsLeft = (timeLeft % 3600000) / 600000; html += String(minutesLeft) + "m " + String(secondsLeft) + "s ]"; html += "
"; html += "
"; html += "
"; html += "> CREATE NEW THREAD <"; html += "
"; html += "
"; // Read and display threads File threadsFile = SD.open("/forum/threads.json", FILE_READ); if (threadsFile) { String threads = threadsFile.readString(); threadsFile.close(); int start = threads.indexOf('['); int end = threads.lastIndexOf(']'); if (start >= 0 && end >= 0) { threads = threads.substring(start + 1, end); while (threads.length() > 0) { int threadEnd = threads.indexOf('}'); if (threadEnd < 0) break; String threadData = threads.substring(0, threadEnd + 1); String threadId = threadData.substring(threadData.indexOf("\"id\":\"") + 6); threadId = threadId.substring(0, threadId.indexOf("\"")); String threadTitle = threadData.substring(threadData.indexOf("\"title\":\"") + 9); threadTitle = threadTitle.substring(0, threadTitle.indexOf("\"")); html += "
"; html += "
"; html += ""; html += "
"; html += "
"; threads = threads.substring(threadEnd + 2); } } } html += "
"; html += "
<< Return to Terminal >>"; html += "
"; html += ""; server.send(200, "text/html", html); } void handleThread() { String threadId = server.arg("id"); if (threadId.length() == 0) { server.sendHeader("Location", "/forum"); server.send(303); return; } String html = ""; html += ""; html += ""; // Add JavaScript for auto-scrolling and post updates html += ""; html += ""; // Find thread title File threadsFile = SD.open("/forum/threads.json", FILE_READ); String threadTitle = "Unknown Thread"; if (threadsFile) { String threads = threadsFile.readString(); threadsFile.close(); int threadStart = threads.indexOf("\"id\":\"" + threadId + "\""); if (threadStart >= 0) { int titleStart = threads.indexOf("\"title\":\"", threadStart) + 9; int titleEnd = threads.indexOf("\"", titleStart); threadTitle = threads.substring(titleStart, titleEnd); } } html += "

// " + threadTitle + " //

"; // Posts container (this part gets refreshed) html += "
"; html += "
"; // Store posts in an array first std::vector postsList; String postsPath = "/forum/posts/" + threadId + ".json"; File postsFile = SD.open(postsPath, FILE_READ); if (postsFile) { String posts = postsFile.readString(); postsFile.close(); int start = posts.indexOf('['); int end = posts.lastIndexOf(']'); if (start >= 0 && end >= 0) { posts = posts.substring(start + 1, end); while (posts.length() > 0) { int postEnd = posts.indexOf('}'); if (postEnd < 0) break; String postData = posts.substring(0, postEnd + 1); postsList.push_back(postData); posts = posts.substring(postEnd + 2); } } } // Display posts in chronological order for (const String& postData : postsList) { String author = postData.substring(postData.indexOf("\"author\":\"") + 10); author = author.substring(0, author.indexOf("\"")); String content = postData.substring(postData.indexOf("\"content\":\"") + 11); content = content.substring(0, content.indexOf("\"")); String timestamp = postData.substring(postData.indexOf("\"timestamp\":\"") + 13); timestamp = timestamp.substring(0, timestamp.indexOf("\"")); html += "
"; html += "
"; html += "[ USER: " + author + " " + formatTimestamp(timestamp.toInt()) + " ]"; html += "
"; html += "
" + content + "
"; html += "
"; } html += "
"; // Close posts-wrapper html += "
"; // Close posts-container // Reply section (outside the refreshing container) html += "
"; html += "
"; html += ""; html += "
"; html += "
"; html += ""; html += "
"; html += "
"; html += "
"; html += "
<< Back to Forum >>"; html += "
"; html += ""; server.send(200, "text/html", html); } void handleThreadAjax() { if (!server.hasArg("ajax")) { handleThread(); return; } String threadId = server.arg("id"); String html = ""; std::vector postsList; String postsPath = "/forum/posts/" + threadId + ".json"; File postsFile = SD.open(postsPath, FILE_READ); if (postsFile) { String posts = postsFile.readString(); postsFile.close(); int start = posts.indexOf('['); int end = posts.lastIndexOf(']'); if (start >= 0 && end >= 0) { posts = posts.substring(start + 1, end); while (posts.length() > 0) { int postEnd = posts.indexOf('}'); if (postEnd < 0) break; String postData = posts.substring(0, postEnd + 1); postsList.push_back(postData); posts = posts.substring(postEnd + 2); } } } // Display posts in chronological order for (const String& postData : postsList) { String author = postData.substring(postData.indexOf("\"author\":\"") + 10); author = author.substring(0, author.indexOf("\"")); String content = postData.substring(postData.indexOf("\"content\":\"") + 11); content = content.substring(0, content.indexOf("\"")); String timestamp = postData.substring(postData.indexOf("\"timestamp\":\"") + 13); timestamp = timestamp.substring(0, timestamp.indexOf("\"")); html += "
"; html += "
"; html += "[ USER: " + author + " " + formatTimestamp(timestamp.toInt()) + " ]"; html += "
"; html += "
" + content + "
"; html += "
"; } server.send(200, "text/html", html); } void handleNewThread() { if (server.method() == HTTP_POST) { String title = server.arg("title"); String content = server.arg("content"); String author = server.arg("author"); if (title.length() > 0 && content.length() > 0) { // Create new thread String threadId = String(millis()); // Read and parse existing threads File threadsFile = SD.open("/forum/threads.json", FILE_READ); String threads = "[]"; if (threadsFile) { threads = threadsFile.readString(); threadsFile.close(); // Remove the last ] threads = threads.substring(0, threads.length() - 1); // Add comma if not empty array if (threads.length() > 1) { threads += ","; } } // Add new thread threads += "{\"id\":\"" + threadId + "\","; threads += "\"title\":\"" + title + "\","; threads += "\"author\":\"" + author + "\","; threads += "\"timestamp\":\"" + String(millis()) + "\"}]"; threadsFile = SD.open("/forum/threads.json", FILE_WRITE); if (threadsFile) { threadsFile.print(threads); threadsFile.close(); } // Save initial post String postPath = "/forum/posts/" + threadId + ".json"; File postFile = SD.open(postPath, FILE_WRITE); if (postFile) { postFile.print("[{\"id\":\"" + String(millis()) + "\","); postFile.print("\"author\":\"" + author + "\","); postFile.print("\"content\":\"" + content + "\","); postFile.print("\"timestamp\":\"" + String(millis()) + "\"}]"); postFile.close(); } server.sendHeader("Location", "/forum/thread?id=" + threadId); server.send(303); return; } } // Display new thread form String html = ""; html += ""; html += ""; html += "

// NEW THREAD //

"; html += "
"; html += "
"; html += "
"; html += "
"; html += ""; html += "
"; html += "
"; html += "
<< Back to Forum >>"; html += "
"; html += ""; server.send(200, "text/html", html); } void handleNewPost() { if (server.method() == HTTP_POST) { String threadId = server.arg("threadId"); String content = server.arg("content"); String author = server.arg("author"); if (threadId.length() > 0 && content.length() > 0) { String postsPath = "/forum/posts/" + threadId + ".json"; // Create new post JSON String newPost = "{\"id\":\"" + String(millis()) + "\","; newPost += "\"author\":\"" + author + "\","; newPost += "\"content\":\"" + content + "\","; newPost += "\"timestamp\":\"" + String(millis()) + "\"}"; // Open file and read existing content File postsFile = SD.open(postsPath, FILE_READ); String currentPosts; if (postsFile) { currentPosts = postsFile.readString(); postsFile.close(); // Delete the file so we can rewrite it SD.remove(postsPath); } else { currentPosts = "[]"; } // Create updated JSON content String updatedPosts; if (currentPosts == "[]") { updatedPosts = "[" + newPost + "]"; } else { // Remove the closing bracket, add new post, then close updatedPosts = currentPosts.substring(0, currentPosts.length() - 1) + "," + newPost + "]"; } // Write the updated content to a new file postsFile = SD.open(postsPath, FILE_WRITE); if (postsFile) { postsFile.print(updatedPosts); postsFile.close(); } } server.sendHeader("Location", "/forum/thread?id=" + threadId + "&scroll=true#bottom"); server.send(303); return; } server.sendHeader("Location", "/forum"); server.send(303); } String formatTimestamp(unsigned long timestamp) { unsigned long timeDiff = millis() - timestamp; if (timeDiff < 60000) return String(timeDiff / 1000) + "s ago"; if (timeDiff < 3600000) return String(timeDiff / 60000) + "m ago"; if (timeDiff < 86400000) return String(timeDiff / 3600000) + "h ago"; return String(timeDiff / 86400000) + "d ago"; } void handleUploadPage() { String html = ""; html += ""; html += ""; // Add JavaScript for progress handling html += ""; html += ""; html += "

// D0N473 //

"; html += "
"; html += "
"; html += "
[ PDF|EPUB|DOC|RTF|TXT ]
"; html += "
"; html += "
"; html += "
"; html += "
0%
"; html += "
"; html += ""; html += "
"; html += "
"; html += "
"; html += "
<< Return to Terminal >>"; html += "
"; html += ""; server.send(200, "text/html", html); } void handleFileUpload() { if (server.uri() != "/upload") return; HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { String filename = upload.filename; if (!isAllowedFile(filename)) { return; } // Create/Open file uploadFile = SD.open(filename, FILE_WRITE); } else if (upload.status == UPLOAD_FILE_WRITE) { if (uploadFile) { uploadFile.write(upload.buf, upload.currentSize); } } else if (upload.status == UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); } } } void handleUpload() { if (server.method() != HTTP_POST) { server.send(405, "text/plain", "Method Not Allowed"); return; } server.sendHeader("Location", "/"); server.send(303); } void handleFileList() { String html = ""; html += ""; html += ""; // Add JavaScript for toggling sections html += ""; html += ""; html += "
"; html += "

// 404 DEWEY NOT FOUND //

"; // Create a map to store files by first letter std::map>> fileGroups; File root = SD.open("/"); if (!root) { html += "

[ERROR] Failed to access storage system

"; } else { while (File file = root.openNextFile()) { String fileName = String(file.name()); if (isAllowedFile(fileName)) { char firstLetter = toupper(fileName.charAt(0)); fileGroups[firstLetter].push_back({fileName, file.size()}); } file.close(); } root.close(); if (fileGroups.empty()) { html += "

[WARNING] No documents found in system

"; } else { // Iterate through groups and display files for (const auto& group : fileGroups) { html += "
"; html += "[" + String(group.first) + "]"; html += "[Files: " + String(group.second.size()) + "]"; html += "
"; html += "
    "; // Sort files within each group std::vector> sortedFiles = group.second; std::sort(sortedFiles.begin(), sortedFiles.end()); for (const auto& file : sortedFiles) { html += "
  • "; html += "> " + file.first + " <"; html += "
    [Size: " + String(file.second / 1024.0, 1) + " KB]
    "; html += "
  • "; } html += "
"; } } } html += "
<< Return to Terminal >>"; html += "
"; server.send(200, "text/html", html); } void handleFileDownload() { if (!server.hasArg("file")) { server.send(400, "text/plain", "File parameter missing"); return; } String fileName = server.arg("file"); if (!isAllowedFile(fileName)) { server.send(400, "text/plain", "Invalid file type. Only PDF, EPUB, DOC, RTF, and TXT files are allowed."); return; } if (!SD.exists(fileName)) { server.send(404, "text/plain", "File not found"); return; } File file = SD.open(fileName, FILE_READ); if (!file) { server.send(500, "text/plain", "Failed to open file"); return; } String contentType = "application/octet-stream"; if (fileName.endsWith(".pdf")) { contentType = "application/pdf"; } else if (fileName.endsWith(".epub")) { contentType = "application/epub+zip"; } else if (fileName.endsWith(".doc")) { contentType = "application/msword"; } else if (fileName.endsWith(".rtf")) { contentType = "application/rtf"; } else if (fileName.endsWith(".txt")) { contentType = "text/plain"; } server.sendHeader("Content-Disposition", "attachment; filename=" + fileName); server.sendHeader("Connection", "close"); server.streamFile(file, contentType); file.close(); } void handleToggle() { ledState = !ledState; digitalWrite(ledPin, ledState); server.sendHeader("Location", "/"); server.send(303); }