#include "net_service/app_net.h" #include "esp_log.h" #include "esp_system.h" #include "esp_http_server.h" #include "esp_netif.h" #include "esp_spiffs.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "mdns.h" #include "nfc_engine/nfc_brute.h" #include "nfc_engine/nfc_deep.h" #include "nfc_engine/nfc_engine.h" #include "nfc_engine/session_capture.h" #include "nvs_flash.h" #include "pn532_host/pn532_core.h" #include "cJSON.h" #include "http_parser.h" #include #include #include #include #include static const char *TAG = "app_net"; /* SoftAP: open network (no password) for lab / fastest join. Change SSID here if you want. */ #define SOFTAP_SSID "PN532-Toolkit" #define MAX_JSON 4096 #define WS_BROADCAST_BUF 4096 static httpd_handle_t s_server; static bool s_scan = true; static TaskHandle_t s_scan_task; static int hexval(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'a' && c <= 'f') { return 10 + c - 'a'; } if (c >= 'A' && c <= 'F') { return 10 + c - 'A'; } return -1; } static bool hex_to_bin(const char *hex, uint8_t *out, size_t out_len) { size_t n = strlen(hex); if (n != out_len * 2) { return false; } for (size_t i = 0; i < out_len; i++) { int h = hexval(hex[i * 2]); int l = hexval(hex[i * 2 + 1]); if (h < 0 || l < 0) { return false; } out[i] = (uint8_t)((h << 4) | l); } return true; } static bool hex_decode_flex(const char *hex, uint8_t *out, size_t out_cap, size_t *out_len) { size_t n = strlen(hex); if (n % 2 || n / 2 > out_cap) { return false; } *out_len = n / 2; return hex_to_bin(hex, out, *out_len); } /** Read full POST body (httpd may return partial data in multiple recv calls). */ static int recv_body_capped(httpd_req_t *req, char *buf, size_t cap) { if (!buf || cap < 2) { return -1; } size_t cl = req->content_len; if (cl >= cap) { return -1; } if (cl == 0) { buf[0] = '\0'; return 0; } size_t got = 0; while (got < cl) { int r = httpd_req_recv(req, buf + got, cl - got); if (r <= 0) { return -1; } got += (size_t)r; } buf[got] = '\0'; return (int)got; } static char *recv_body_alloc(httpd_req_t *req, size_t max_len, int *out_len) { size_t cl = req->content_len; if (cl == 0 || cl > max_len) { return NULL; } char *buf = malloc(cl + 1); if (!buf) { return NULL; } size_t got = 0; while (got < cl) { int r = httpd_req_recv(req, buf + got, cl - got); if (r <= 0) { free(buf); return NULL; } got += (size_t)r; } buf[cl] = '\0'; if (out_len) { *out_len = (int)cl; } return buf; } esp_err_t app_net_broadcast_json(const char *channel, const char *json_text) { (void)channel; if (!s_server || !json_text) { return ESP_ERR_INVALID_STATE; } char line[WS_BROADCAST_BUF]; int n = snprintf(line, sizeof(line), "{\"channel\":\"%s\",\"payload\":%s}", channel ? channel : "event", json_text); if (n < 0 || n >= (int)sizeof(line)) { return ESP_ERR_NO_MEM; } int fds[16]; size_t fdcount = sizeof(fds) / sizeof(fds[0]); esp_err_t er = httpd_get_client_list(s_server, &fdcount, fds); if (er != ESP_OK) { return er; } httpd_ws_frame_t pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)line, .len = (size_t)n}; for (size_t i = 0; i < fdcount; i++) { if (httpd_ws_get_fd_info(s_server, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) { (void)httpd_ws_send_frame_async(s_server, fds[i], &pkt); } } return ESP_OK; } static esp_err_t send_json(httpd_req_t *req, cJSON *j, int status) { char *p = cJSON_PrintUnformatted(j); cJSON_Delete(j); if (!p) { return ESP_ERR_NO_MEM; } httpd_resp_set_type(req, "application/json"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS"); httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); httpd_resp_set_status(req, status == 200 ? "200 OK" : "400 Bad Request"); esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN); free(p); return e; } static esp_err_t api_status(httpd_req_t *req) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "app", "pn532_nfc_toolkit"); cJSON_AddNumberToObject(o, "uptimeMs", (double)(esp_timer_get_time() / 1000)); cJSON_AddNumberToObject(o, "freeHeap", (double)esp_get_free_heap_size()); wifi_mode_t mode; esp_wifi_get_mode(&mode); cJSON_AddNumberToObject(o, "wifiMode", mode); uint8_t ic = 0, hi = 0, lo = 0; if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) { cJSON *pn = cJSON_CreateObject(); cJSON_AddNumberToObject(pn, "ic", ic); cJSON_AddNumberToObject(pn, "fwHi", hi); cJSON_AddNumberToObject(pn, "fwLo", lo); cJSON_AddItemToObject(o, "pn532", pn); } cJSON_AddBoolToObject(o, "scanning", s_scan); size_t cap_u = 0; uint32_t cap_l = 0; bool cap_f = false; session_capture_get_status(&cap_u, &cap_l, &cap_f); cJSON *cap = cJSON_CreateObject(); cJSON_AddNumberToObject(cap, "usedBytes", (double)cap_u); cJSON_AddNumberToObject(cap, "maxBytes", (double)session_capture_max()); cJSON_AddNumberToObject(cap, "lines", (double)cap_l); cJSON_AddBoolToObject(cap, "full", cap_f); cJSON_AddBoolToObject(cap, "deepCapture", session_capture_deep_enabled()); cJSON_AddItemToObject(o, "session", cap); return send_json(req, o, 200); } static void tag_uid_hex(const nfc_tag_info_t *tag, char *out, size_t out_sz) { if (!out || out_sz == 0) { return; } out[0] = 0; size_t p = 0; for (int i = 0; i < tag->uid_len && p + 2 < out_sz; i++) { p += (size_t)snprintf(out + p, out_sz - p, "%02X", tag->uid[i]); } } static esp_err_t api_session_export(httpd_req_t *req) { size_t n = session_capture_export_size(); if (n == 0) { httpd_resp_set_type(req, "text/plain"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); return httpd_resp_send(req, "empty", HTTPD_RESP_USE_STRLEN); } char *buf = malloc(n); if (!buf) { return send_json(req, cJSON_CreateString("out of memory"), 400); } size_t got = 0; session_capture_copy_to(buf, n, &got); httpd_resp_set_type(req, "application/x-ndjson"); httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"pn532-deep-capture.ndjson\""); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); esp_err_t er = httpd_resp_send(req, buf, got); free(buf); return er; } static esp_err_t api_session_clear(httpd_req_t *req) { char drain[128]; (void)recv_body_capped(req, drain, sizeof(drain)); session_capture_clear(); cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "ok", true); return send_json(req, o, 200); } static esp_err_t api_session_deep(httpd_req_t *req) { char buf[128]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("body required or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *en = cJSON_GetObjectItem(j, "enable"); if (cJSON_IsBool(en)) { session_capture_set_deep(cJSON_IsTrue(en)); } cJSON_Delete(j); cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "deepCapture", session_capture_deep_enabled()); return send_json(req, o, 200); } static esp_err_t api_nfc_poll(httpd_req_t *req) { char drain[128]; (void)recv_body_capped(req, drain, sizeof(drain)); nfc_tag_info_t tag; esp_err_t e = nfc_poll_passive_target(&tag); if (e == ESP_ERR_NOT_FOUND) { cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "present", false); return send_json(req, o, 200); } if (e != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(e)); return send_json(req, o, 400); } cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "present", true); cJSON_AddItemToObject(o, "tag", nfc_tag_to_json(&tag)); return send_json(req, o, 200); } static esp_err_t api_scan(httpd_req_t *req) { char buf[128]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *en = cJSON_GetObjectItem(j, "enable"); if (cJSON_IsBool(en)) { app_net_set_continuous_scan(cJSON_IsTrue(en)); } cJSON_Delete(j); cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "enable", s_scan); return send_json(req, o, 200); } static esp_err_t api_general_status(httpd_req_t *req) { uint8_t gs[32]; size_t gl = 0; esp_err_t e = pn532_get_general_status(gs, sizeof(gs), &gl); if (e != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(e)); return send_json(req, o, 400); } cJSON *o = cJSON_CreateObject(); cJSON *arr = cJSON_CreateArray(); for (size_t i = 0; i < gl; i++) { cJSON_AddItemToArray(arr, cJSON_CreateNumber(gs[i])); } cJSON_AddItemToObject(o, "raw", arr); return send_json(req, o, 200); } static esp_err_t api_mifare_read(httpd_req_t *req) { char buf[512]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jbk = cJSON_GetObjectItem(j, "block"); cJSON *jk = cJSON_GetObjectItem(j, "key"); int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1; const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL; cJSON *jb = cJSON_GetObjectItem(j, "keyB"); bool key_b = cJSON_IsTrue(jb); cJSON_Delete(j); if (block < 0 || !key_hex) { return send_json(req, cJSON_CreateString("block/key required"), 400); } uint8_t keyb[6]; if (!hex_to_bin(key_hex, keyb, 6)) { return send_json(req, cJSON_CreateString("key must be 12 hex chars"), 400); } nfc_tag_info_t tag; if (nfc_poll_passive_target(&tag) != ESP_OK) { return send_json(req, cJSON_CreateString("no tag"), 400); } nfc_mifare_key_t k = {.key_b = key_b}; memcpy(k.key, keyb, 6); if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) { return send_json(req, cJSON_CreateString("auth failed"), 400); } uint8_t blk[NFC_BLOCK_LEN]; if (nfc_mifare_read_block((uint8_t)block, blk) != ESP_OK) { return send_json(req, cJSON_CreateString("read failed"), 400); } char hexout[NFC_BLOCK_LEN * 2 + 1]; for (int i = 0; i < NFC_BLOCK_LEN; i++) { snprintf(hexout + i * 2, 3, "%02X", blk[i]); } cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "block", block); cJSON_AddStringToObject(o, "data", hexout); return send_json(req, o, 200); } static esp_err_t api_mifare_write(httpd_req_t *req) { char buf[512]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jbk = cJSON_GetObjectItem(j, "block"); cJSON *jk = cJSON_GetObjectItem(j, "key"); cJSON *jd = cJSON_GetObjectItem(j, "data"); int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1; const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL; const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL; cJSON *jb = cJSON_GetObjectItem(j, "keyB"); bool key_b = cJSON_IsTrue(jb); cJSON_Delete(j); if (block < 0 || !key_hex || !data_hex) { return send_json(req, cJSON_CreateString("block/key/data required"), 400); } uint8_t keyb[6], blk[NFC_BLOCK_LEN]; if (!hex_to_bin(key_hex, keyb, 6) || !hex_to_bin(data_hex, blk, NFC_BLOCK_LEN)) { return send_json(req, cJSON_CreateString("bad hex"), 400); } nfc_tag_info_t tag; if (nfc_poll_passive_target(&tag) != ESP_OK) { return send_json(req, cJSON_CreateString("no tag"), 400); } nfc_mifare_key_t k = {.key_b = key_b}; memcpy(k.key, keyb, 6); if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) { return send_json(req, cJSON_CreateString("auth failed"), 400); } if (nfc_mifare_write_block((uint8_t)block, blk) != ESP_OK) { return send_json(req, cJSON_CreateString("write failed"), 400); } return send_json(req, cJSON_CreateObject(), 200); } static esp_err_t api_ul_read(httpd_req_t *req) { char buf[128]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jp = cJSON_GetObjectItem(j, "page"); int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1; cJSON_Delete(j); if (page < 0) { return send_json(req, cJSON_CreateString("page required"), 400); } uint8_t d[4]; if (nfc_ultralight_read_page((uint8_t)page, d) != ESP_OK) { return send_json(req, cJSON_CreateString("read failed"), 400); } char hx[9]; snprintf(hx, sizeof hx, "%02X%02X%02X%02X", d[0], d[1], d[2], d[3]); cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "page", page); cJSON_AddStringToObject(o, "data", hx); return send_json(req, o, 200); } static esp_err_t api_ul_write(httpd_req_t *req) { char buf[128]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jp = cJSON_GetObjectItem(j, "page"); cJSON *jd = cJSON_GetObjectItem(j, "data"); int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1; const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL; cJSON_Delete(j); if (page < 0 || !data_hex) { return send_json(req, cJSON_CreateString("page and data (8 hex) required"), 400); } uint8_t d[4]; if (!hex_to_bin(data_hex, d, 4)) { return send_json(req, cJSON_CreateString("data must be 8 hex chars (4 bytes)"), 400); } if (nfc_ultralight_write_page((uint8_t)page, d) != ESP_OK) { return send_json(req, cJSON_CreateString("write failed"), 400); } cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "page", page); cJSON_AddBoolToObject(o, "ok", true); return send_json(req, o, 200); } static esp_err_t api_ota_stub(httpd_req_t *req) { (void)req; cJSON *o = cJSON_CreateString("Use idf.py app-flash or extend with esp_https_ota + bundle URL"); char *p = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!p) { return ESP_ERR_NO_MEM; } httpd_resp_set_type(req, "application/json"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_status(req, "501 Not Implemented"); esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN); free(p); return e; } static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req) { char *body = recv_body_alloc(req, 8192, NULL); if (!body) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(body); free(body); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jr = cJSON_GetObjectItem(j, "readerType"); const char *rtype = cJSON_IsString(jr) ? jr->valuestring : NULL; uint8_t s0 = 0; uint8_t s1 = 15; cJSON *jf = cJSON_GetObjectItem(j, "sectorFirst"); cJSON *jl = cJSON_GetObjectItem(j, "sectorLast"); if (cJSON_IsNumber(jf)) { s0 = (uint8_t)cJSON_GetNumberValue(jf); } if (cJSON_IsNumber(jl)) { s1 = (uint8_t)cJSON_GetNumberValue(jl); } else if (rtype && strcmp(rtype, "classic4k") == 0) { s1 = 39; } bool variations = cJSON_IsTrue(cJSON_GetObjectItem(j, "variations")); uint8_t extra[96 * 6]; size_t extra_n = 0; cJSON *keys = cJSON_GetObjectItem(j, "keysHex"); if (cJSON_IsArray(keys)) { int n = cJSON_GetArraySize(keys); for (int i = 0; i < n && extra_n < 96; i++) { cJSON *it = cJSON_GetArrayItem(keys, i); if (!cJSON_IsString(it)) { continue; } if (hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) { extra_n++; } } } cJSON_Delete(j); nfc_tag_info_t tag; if (nfc_poll_passive_target(&tag) != ESP_OK) { return send_json(req, cJSON_CreateString("no tag present"), 400); } int attempts = 0; cJSON *out = nfc_mifare_dictionary_attack(&tag, s0, s1, extra_n ? extra : NULL, extra_n, variations, &attempts); if (!out) { return send_json(req, cJSON_CreateString("attack failed"), 400); } char *p = cJSON_PrintUnformatted(out); cJSON_Delete(out); if (!p) { return send_json(req, cJSON_CreateString("print failed"), 400); } httpd_resp_set_type(req, "application/json"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_status(req, "200 OK"); esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN); free(p); return e; } static esp_err_t api_nfc_emulate_raw(httpd_req_t *req) { char buf[1024]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jh = cJSON_GetObjectItem(j, "hex"); const char *hex = cJSON_IsString(jh) ? jh->valuestring : NULL; cJSON_Delete(j); uint8_t bin[260]; size_t blen = 0; if (!hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) { return send_json(req, cJSON_CreateString("hex command required"), 400); } uint8_t resp[300]; size_t rlen = 0; esp_err_t err = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 800); if (err != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 400); } char *rh = calloc(1, rlen * 2 + 1); for (size_t i = 0; i < rlen; i++) { snprintf(rh + i * 2, 3, "%02X", resp[i]); } cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "response", rh); free(rh); return send_json(req, o, 200); } static esp_err_t api_raw_pn532(httpd_req_t *req) { char buf[1024]; int r = recv_body_capped(req, buf, sizeof(buf)); if (r < 0) { return send_json(req, cJSON_CreateString("no body or too large"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *jf = cJSON_GetObjectItem(j, "frame"); const char *hex = cJSON_IsString(jf) ? jf->valuestring : NULL; cJSON_Delete(j); size_t blen = 0; uint8_t bin[260]; if (!hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) { return send_json(req, cJSON_CreateString("frame hex required, even length, max 260B"), 400); } uint8_t resp[300]; size_t rlen = 0; esp_err_t e = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 300); if (e != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(e)); return send_json(req, o, 400); } char *rh = calloc(1, rlen * 2 + 1); for (size_t i = 0; i < rlen; i++) { snprintf(rh + i * 2, 3, "%02X", resp[i]); } cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "response", rh); free(rh); return send_json(req, o, 200); } static esp_err_t api_cors_preflight(httpd_req_t *req) { (void)req; httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS"); httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); httpd_resp_set_hdr(req, "Access-Control-Max-Age", "86400"); httpd_resp_set_status(req, "204 No Content"); return httpd_resp_send(req, "", 0); } static esp_err_t static_any(httpd_req_t *req) { if (strcmp(req->uri, "/") == 0) { httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); FILE *f = fopen("/spiffs/index.html", "r"); if (!f) { httpd_resp_send(req, "

PN532 Toolkit

Build web UI into /data

", HTTPD_RESP_USE_STRLEN); return ESP_OK; } char buf[512]; size_t n; httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) { fclose(f); return ESP_FAIL; } } fclose(f); return httpd_resp_send_chunk(req, NULL, 0); } if (strstr(req->uri, "..") != NULL) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad path"); return ESP_FAIL; } char path[96]; snprintf(path, sizeof path, "/spiffs%s", req->uri); struct stat st; if (stat(path, &st) != 0) { httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found"); return ESP_FAIL; } FILE *f = fopen(path, "r"); if (!f) { httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found"); return ESP_FAIL; } if (strstr(req->uri, ".js")) { httpd_resp_set_type(req, "application/javascript"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); } else if (strstr(req->uri, ".css")) { httpd_resp_set_type(req, "text/css"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); } char buf[512]; size_t n; while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) { fclose(f); return ESP_FAIL; } } fclose(f); return httpd_resp_send_chunk(req, NULL, 0); } static esp_err_t ws_handler(httpd_req_t *req) { if (req->method == HTTP_GET) { ESP_LOGI(TAG, "WS handshake"); return ESP_OK; } httpd_ws_frame_t ws = {.type = HTTPD_WS_TYPE_TEXT}; uint8_t buf[128]; ws.payload = buf; esp_err_t ret = httpd_ws_recv_frame(req, &ws, sizeof(buf)); if (ret != ESP_OK) { return ret; } if (ws.type == HTTPD_WS_TYPE_TEXT && ws.len < sizeof(buf)) { buf[ws.len] = 0; if (strcmp((char *)buf, "ping") == 0) { ws.type = HTTPD_WS_TYPE_TEXT; ws.payload = (uint8_t *)"{\"channel\":\"pong\"}"; ws.len = strlen((char *)ws.payload); return httpd_ws_send_frame(req, &ws); } } return ESP_OK; } static void scan_loop_task(void *arg) { (void)arg; nfc_tag_info_t last; memset(&last, 0, sizeof(last)); while (1) { if (!s_scan) { vTaskDelay(pdMS_TO_TICKS(200)); continue; } if (session_capture_deep_enabled() && session_capture_is_full()) { vTaskDelay(pdMS_TO_TICKS(400)); continue; } nfc_tag_info_t tag; esp_err_t e = nfc_poll_passive_target(&tag); if (e == ESP_OK) { bool is_new = (last.uid_len != tag.uid_len || memcmp(last.uid, tag.uid, tag.uid_len) != 0); if (is_new) { last = tag; cJSON *j = nfc_tag_to_json(&tag); char *raw = cJSON_PrintUnformatted(j); cJSON_Delete(j); if (raw) { app_net_broadcast_json("scan", raw); free(raw); } if (session_capture_deep_enabled() && !session_capture_is_full()) { cJSON *deep = nfc_tag_deep_profile(&tag); char *line = deep ? cJSON_PrintUnformatted(deep) : NULL; cJSON_Delete(deep); if (line) { if (!session_capture_append_line(line)) { cJSON *mini = cJSON_CreateObject(); cJSON_AddStringToObject(mini, "event", "bufferFull"); cJSON_AddBoolToObject(mini, "paused", true); char *m = cJSON_PrintUnformatted(mini); cJSON_Delete(mini); if (m) { app_net_broadcast_json("capture", m); free(m); } } else { size_t u = 0; uint32_t lc = 0; bool f = false; session_capture_get_status(&u, &lc, &f); char uh[32]; tag_uid_hex(&tag, uh, sizeof uh); cJSON *ev = cJSON_CreateObject(); cJSON_AddStringToObject(ev, "event", "recorded"); cJSON_AddStringToObject(ev, "uid", uh); cJSON_AddNumberToObject(ev, "usedBytes", (double)u); cJSON_AddNumberToObject(ev, "lines", (double)lc); cJSON_AddBoolToObject(ev, "full", f); char *es = cJSON_PrintUnformatted(ev); cJSON_Delete(ev); if (es) { app_net_broadcast_json("capture", es); free(es); } } free(line); } } } } else { if (last.uid_len) { memset(&last, 0, sizeof(last)); app_net_broadcast_json("scan", "{\"present\":false}"); } } vTaskDelay(pdMS_TO_TICKS(session_capture_deep_enabled() ? 220 : 65)); } } void app_net_set_continuous_scan(bool on) { s_scan = on; } bool app_net_continuous_scan(void) { return s_scan; } static httpd_handle_t start_server(void) { httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.lru_purge_enable = true; cfg.max_uri_handlers = 48; cfg.stack_size = 8192; httpd_handle_t s = NULL; if (httpd_start(&s, &cfg) != ESP_OK) { return NULL; } httpd_uri_t u = {.uri = "/*", .method = HTTP_OPTIONS, .handler = api_cors_preflight}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/nfc/poll", .method = HTTP_POST, .handler = api_nfc_poll}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/mifare/read-block", .method = HTTP_POST, .handler = api_mifare_read}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/mifare/write-block", .method = HTTP_POST, .handler = api_mifare_write}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/ul/read-page", .method = HTTP_POST, .handler = api_ul_read}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/ul/write-page", .method = HTTP_POST, .handler = api_ul_write}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/raw/pn532", .method = HTTP_POST, .handler = api_raw_pn532}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_stub}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/session/export", .method = HTTP_GET, .handler = api_session_export}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/session/clear", .method = HTTP_POST, .handler = api_session_clear}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/session/deep", .method = HTTP_POST, .handler = api_session_deep}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/mifare/dictionary-attack", .method = HTTP_POST, .handler = api_mifare_dictionary_attack}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/api/nfc/emulate-raw", .method = HTTP_POST, .handler = api_nfc_emulate_raw}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true}; httpd_register_uri_handler(s, &u); u = (httpd_uri_t){.uri = "/*", .method = HTTP_GET, .handler = static_any}; httpd_register_uri_handler(s, &u); return s; } esp_err_t app_net_init(void) { ESP_ERROR_CHECK(nvs_flash_init()); esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_ap(); wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&wcfg)); wifi_config_t ap = {0}; strncpy((char *)ap.ap.ssid, SOFTAP_SSID, sizeof(ap.ap.ssid)); ap.ap.ssid_len = (uint8_t)strlen(SOFTAP_SSID); ap.ap.channel = 6; ap.ap.max_connection = 8; ap.ap.authmode = WIFI_AUTH_OPEN; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap)); ESP_ERROR_CHECK(esp_wifi_start()); esp_vfs_spiffs_conf_t sp = { .base_path = "/spiffs", .partition_label = "storage", .max_files = 16, .format_if_mount_failed = true, }; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&sp)); mdns_init(); mdns_hostname_set("pn532tool"); mdns_instance_name_set("PN532 NFC Toolkit"); mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); s_server = start_server(); if (!s_server) { return ESP_FAIL; } xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task); return ESP_OK; }