// Start using chrono when C++20 becomes available // #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Stop using these when C++20 becomes available #include #include #include "httplib.h" #include "json.hpp" #include "helpers.h" #include "TRMNL.h" using nlohmann::json; using namespace std; namespace fs = std::filesystem; constexpr int API_KEY_LENGHT = 16; constexpr char DEFAULT_IMAGE_URL[] = "https://trmnl.com/images/setup/setup-logo.bmp"; constexpr char DEFAULT_IMAGE_FNAME[] = "2024-09-20T00:00:00"; string generate_api_key(int len = API_KEY_LENGHT) { string result = ""; std::random_device rand; std::uniform_int_distribution distribution(0, 63); int single; char symbol; if (len < 0) { len = API_KEY_LENGHT; } for (int i = 0; i < len; ++i) { single = distribution(rand); if (single <= 9) { symbol = '0' + single; } else if ((single >= 10) && (single <= 35)) { symbol = 'A' + single - 10; } else if ((single >= 36) && (single <= 61)) { symbol = 'a' + single - 36; } else if (62 == single) { symbol = '+'; } else if (63 == single) { symbol = '/'; } // This should never happen else { symbol = '?'; } result += symbol; } return result; } bool reload_container(TRMNLContainer& container, const string& filename) { json j; bool ok = read_file_json(j, filename, &cout); if (!ok) { return false; } container.clear(); if (j.is_array()) { for (int i = 0; i < j.size(); ++i) { container.add_device(j[i]); } } return true; } string get_timestamp_from_filename(const string& filepath) { char result[128] = ""; struct stat status; stat(filepath.c_str(), &status); // Time of last modification time_t file_time = status.st_mtim.tv_sec; tm* file_tm; file_tm = gmtime(&file_time); snprintf(result, sizeof(result) - 1, "%04d-%02d-%02dT%02d:%02d:%02d", file_tm->tm_year + 1900, file_tm->tm_mon + 1, file_tm->tm_mday, file_tm->tm_hour, file_tm->tm_min, file_tm->tm_sec ); return result; } // Use this when C++20 becomes available #if 0 string get_timestamp_from_file_time(const fs::file_time_type& ftime) { char result[128] = ""; time_t file_time = std::chrono::system_clock::to_time_t(std::chrono::file_clock::to_sys(ftime)); tm* file_tm; file_tm = gmtime(&file_time); snprintf(result, sizeof(result) - 1, "%04d-%02d-%02dT%02d:%02d:%02d", file_tm->tm_year + 1900, file_tm->tm_mon + 1, file_tm->tm_mday, file_tm->tm_hour, file_tm->tm_min, file_tm->tm_sec ); return result; } #endif // First string is image filename for URL // Second string is timestamp for TRMNL filename pair find_image_for_friendly(const string& folder_images, const string& friendly_id) { pair result = {"", ""}; set permitted_extensions = { ".bmp", ".png" }; if (!fs::is_directory(folder_images)) { return result; } for (const fs::directory_entry& entry : fs::directory_iterator(folder_images)) { if ((entry.path().stem() == friendly_id) && (0 != permitted_extensions.count(entry.path().extension()))) { result.first = entry.path().filename(); result.second = get_timestamp_from_filename(entry.path()); // Change to this when C++20 becomes available // result.second = get_timestamp_from_file_time(entry.last_write_time()); } } return result; } int main(int argc, char **argv) { string config_filename = "config.json"; string devices_filename = "devices.json"; string folder_images = "images"; string host = ""; uint16_t port = 0; string base_url = ""; string cert_file = ""; string key_file = ""; shared_ptr server = nullptr; bool ok; json cfg; json devs; TRMNLContainer container; if (argc > 2) { config_filename = argv[1]; } ok = read_file_json(cfg, config_filename, &cout); if (!ok) { return -1; } json_extract(cfg, "host", host); json_extract(cfg, "port", port); json_extract(cfg, "base_url", base_url); json_extract(cfg, "devices_filename", devices_filename); json_extract(cfg, "folder_images", folder_images); // TODO: Extract when SSL is ready // json_extract(cfg, "cert_file", cert_file); // json_extract(cfg, "key_file", cert_file); if (host.empty()) { cout << "host not provided" << endl; return -1; } if (0 == port) { cout << "port number not provided" << endl; return -1; } if (base_url.empty()) { cout << "base url not provided" << endl; return -1; } if (folder_images.empty()) { cout << "folder for images is empty" << endl; return -1; } if (!fs::is_directory(folder_images)) { cout << "filepath for images is not a folder" << endl; return -1; } if (!cert_file.empty() && !key_file.empty()) { // TODO: Implement SSL Server Properly } else { server = make_shared(); } ok = reload_container(container, devices_filename); if (!ok) { cout << "Could not read devices file" << endl; return -1; } auto setup_handler = [&container, &devices_filename](const httplib::Request& req, httplib::Response& res) { json response; if (!req.has_header("ID")) { // Bad Request - No ID header res.status = 400; response["status"] = 400; response["error"] = "No ID header"; res.set_header("Content-Type", "application/json"); res.body = response.dump(); return; } // Refresh data from file // Someone might have put a new device in reload_container(container, devices_filename); string id = req.get_header_value("ID"); TRMNL* trmnl = container.get_device_by_id(id); if (nullptr == trmnl) { res.status = 404; response["status"] = 404; response["api_key"] = nullptr; response["friendly_id"] = nullptr; response["image_url"] = nullptr; response["filename"] = nullptr; res.set_header("Content-Type", "application/json"); res.body = response.dump(); return; } bool should_dump = false; res.status = 200; if (trmnl->friendly_id().empty()) { should_dump = true; trmnl->friendly_id(TRMNL::friendly_from_id(id)); } if (trmnl->api_key().empty()) { should_dump = true; trmnl->api_key(generate_api_key()); } response["status"] = 200; response["api_key"] = trmnl->api_key(); response["friendly_id"] = trmnl->friendly_id(); response["image_url"] = DEFAULT_IMAGE_URL; response["filename"] = DEFAULT_IMAGE_FNAME; res.set_header("Content-Type", "application/json"); res.body = response.dump(); if (should_dump) { write_file_json(container, devices_filename, &cout); } }; auto display_handler = [&container, &devices_filename, &folder_images, &base_url] (const httplib::Request& req, httplib::Response& res) { json response; if (!req.has_header("ID") || !req.has_header("Access-Token")) { // Bad Request res.status = 400; response["status"] = 400; response["error"] = "No ID and Access-Token headers set"; res.body = response.dump(); res.set_header("Content-Type", "application/json"); return; } // Refresh data from file // Someone might have put a new device in reload_container(container, devices_filename); string id = req.get_header_value("ID"); string api_key = req.get_header_value("Access-Token"); TRMNL* trmnl = container.get_device_by_id(id); if (nullptr == trmnl) { // Not Found res.status = 404; response["status"] = 404; response["error"] = "Device not found"; res.set_header("Content-Type", "application/json"); res.body = response.dump(); return; } if (trmnl->api_key() != api_key) { // Forbidden res.status = 403; response["status"] = 403; response["error"] = "Wrong credentials for this device"; res.set_header("Content-Type", "application/json"); res.body = response.dump(); return; } string image_fname; string image_timestamp; tie(image_fname, image_timestamp) = find_image_for_friendly(folder_images, trmnl->friendly_id()); // The ID and api_key match here res.status = 200; // From docs: will be 202 if no user_id is attached to device response["status"] = 0; if (image_fname.empty()) { response["image_url"] = DEFAULT_IMAGE_URL; response["filename"] = DEFAULT_IMAGE_FNAME; } else { string full_url = base_url; full_url += "/images/"; full_url += image_fname; response["image_url"] = full_url; response["filename"] = image_timestamp; } response["refresh_rate"] = to_string(trmnl->refresh_rate()); // TODO: Handle firmware updating response["update_firmware"] = false; response["firmware_url"] = nullptr; response["reset_firmware"] = false; res.set_header("Content-Type", "application/json"); res.body = response.dump(); }; auto log_handler = [](const httplib::Request& req, httplib::Response& res) { try { json j = json::parse(req.body); cout << j.dump(4) << endl; } catch (const exception& e) { cout << req.body << endl; } res.status = 200; }; server->Get("/api/setup/*", setup_handler); server->Get("/api/display/*", display_handler); server->Post("/api/log/*", log_handler); ok = server->set_mount_point("/images", folder_images); if (!ok) { cout << "Could not mount images folder" << endl; return -1; } cout << "Server listening on " << host << ":" << port << endl; server->listen(host, port); return 0; }