#include "net_service/app_net.h" #include "esp_event.h" #include "esp_log.h" #include "esp_crt_bundle.h" #include "esp_https_ota.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/semphr.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 #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 WS_BROADCAST_BUF 4096 #define STATIC_PATH_MAX 192 static httpd_handle_t s_server; static SemaphoreHandle_t s_nfc_op_mu; static volatile bool s_scan = true; static TaskHandle_t s_scan_task; static volatile bool s_target_active; static void nfc_access_lock(void) { if (s_nfc_op_mu) { xSemaphoreTake(s_nfc_op_mu, portMAX_DELAY); } } static void nfc_access_unlock(void) { if (s_nfc_op_mu) { xSemaphoreGive(s_nfc_op_mu); } } 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); } static bool copy_json_string(const cJSON *item, char *dst, size_t cap) { if (!dst || cap == 0 || !cJSON_IsString(item) || !item->valuestring) { return false; } size_t len = strlen(item->valuestring); if (len >= cap) { return false; } memcpy(dst, item->valuestring, len + 1); return true; } static void bin_to_hex(const uint8_t *src, size_t len, char *dst, size_t dst_cap) { if (!dst || dst_cap == 0) { return; } if (!src || dst_cap < (len * 2 + 1)) { dst[0] = '\0'; return; } for (size_t i = 0; i < len; i++) { snprintf(dst + i * 2, 3, "%02X", src[i]); } dst[len * 2] = '\0'; } static bool add_hex_to_object(cJSON *obj, const char *name, const uint8_t *src, size_t len) { if (!obj || !name || !src) { return false; } char *hex = calloc(1, len * 2 + 1); if (!hex) { return false; } bin_to_hex(src, len, hex, len * 2 + 1); cJSON_AddStringToObject(obj, name, hex); free(hex); return true; } static int json_get_int_default(const cJSON *obj, const char *name, int fallback) { cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name); return cJSON_IsNumber(item) ? (int)cJSON_GetNumberValue(item) : fallback; } static bool json_get_bool_default(const cJSON *obj, const char *name, bool fallback) { cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name); return cJSON_IsBool(item) ? cJSON_IsTrue(item) : fallback; } /** 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) { 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_data(s_server, fds[i], &pkt); } } return ESP_OK; } static esp_err_t send_json(httpd_req_t *req, cJSON *j, int status) { if (!req || !j) { cJSON_Delete(j); return ESP_ERR_INVALID_ARG; } 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"); switch (status) { case 200: httpd_resp_set_status(req, "200 OK"); break; case 400: httpd_resp_set_status(req, "400 Bad Request"); break; case 404: httpd_resp_set_status(req, "404 Not Found"); break; case 409: httpd_resp_set_status(req, "409 Conflict"); break; case 500: httpd_resp_set_status(req, "500 Internal Server Error"); break; case 501: httpd_resp_set_status(req, "501 Not Implemented"); break; default: httpd_resp_set_status(req, "200 OK"); break; } esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN); free(p); return e; } static esp_err_t ensure_ultralight_tag_locked(nfc_tag_info_t *tag) { if (!tag) { return ESP_ERR_INVALID_ARG; } esp_err_t err = nfc_poll_passive_target(tag); if (err != ESP_OK) { return err; } return nfc_tag_is_type2(tag) ? ESP_OK : ESP_ERR_NOT_SUPPORTED; } static esp_err_t ensure_mifare_classic_tag_locked(nfc_tag_info_t *tag) { if (!tag) { return ESP_ERR_INVALID_ARG; } esp_err_t err = nfc_poll_passive_target(tag); if (err != ESP_OK) { return err; } return nfc_tag_is_mifare_classic(tag) ? ESP_OK : ESP_ERR_NOT_SUPPORTED; } static esp_err_t reject_if_target_active(httpd_req_t *req) { if (!s_target_active) { return ESP_OK; } return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409); } static cJSON *build_probe_locked(const nfc_tag_info_t *tag) { if (!tag) { return NULL; } cJSON *root = cJSON_CreateObject(); cJSON *tag_json = nfc_tag_to_json(tag); cJSON *caps = cJSON_CreateObject(); if (!root || !tag_json || !caps) { cJSON_Delete(root); cJSON_Delete(tag_json); cJSON_Delete(caps); return NULL; } cJSON_AddItemToObject(root, "tag", tag_json); if (nfc_tag_is_mifare_classic(tag)) { cJSON_AddStringToObject(caps, "family", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic"); cJSON_AddNumberToObject(caps, "sectorCount", nfc_mifare_sector_count(tag)); cJSON_AddBoolToObject(caps, "canRead", true); cJSON_AddBoolToObject(caps, "canWrite", true); cJSON_AddBoolToObject(caps, "canDictionaryAttack", true); cJSON_AddBoolToObject(caps, "structuredCloneCapture", true); cJSON_AddBoolToObject(caps, "structuredCloneProgram", true); cJSON_AddBoolToObject(caps, "manufacturerBlockWritable", false); cJSON_AddBoolToObject(caps, "targetModeRelevant", false); cJSON_AddStringToObject(caps, "note", "Classic clone/program flow can write user blocks and optional trailers, but not manufacturer block 0 on normal cards."); } else if (nfc_tag_is_type2(tag)) { cJSON_AddStringToObject(caps, "family", "type2"); cJSON_AddBoolToObject(caps, "canRead", true); cJSON_AddBoolToObject(caps, "canWrite", true); cJSON_AddBoolToObject(caps, "structuredCloneCapture", true); cJSON_AddBoolToObject(caps, "structuredCloneProgram", true); cJSON_AddNumberToObject(caps, "safeUserPageStart", 4); uint8_t version[8]; if (nfc_type2_get_version(version) == ESP_OK) { (void)add_hex_to_object(caps, "getVersionHex", version, sizeof(version)); } cJSON_AddStringToObject(caps, "note", "Type 2 clone/program flow writes user pages by default and leaves manufacturer/lock pages untouched unless forced."); } else { cJSON_AddStringToObject(caps, "family", "unknown"); cJSON_AddBoolToObject(caps, "canRead", false); cJSON_AddBoolToObject(caps, "canWrite", false); cJSON_AddBoolToObject(caps, "structuredCloneCapture", false); cJSON_AddBoolToObject(caps, "structuredCloneProgram", false); cJSON_AddStringToObject(caps, "note", "Use raw commands and manual analysis; this tag does not match the current structured Classic/Type 2 workflows."); } cJSON_AddItemToObject(root, "capabilities", caps); return root; } static cJSON *find_sector_hit(const cJSON *attack, int sector) { if (!attack) { return NULL; } cJSON *hits = cJSON_GetObjectItemCaseSensitive((cJSON *)attack, "sectorHits"); if (!cJSON_IsArray(hits)) { return NULL; } int n = cJSON_GetArraySize(hits); for (int i = 0; i < n; i++) { cJSON *it = cJSON_GetArrayItem(hits, i); if (!it) continue; cJSON *s = cJSON_GetObjectItemCaseSensitive(it, "sector"); if (cJSON_IsNumber(s) && (int)cJSON_GetNumberValue(s) == sector) { return it; } } return NULL; } static bool parse_sector_key_hit(const cJSON *hit, nfc_mifare_key_t *key, char *key_hex_out, size_t key_hex_cap) { if (!hit || !key) { return false; } cJSON *kh = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyHex"); cJSON *kt = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyType"); if (!cJSON_IsString(kh) || !kh->valuestring || !cJSON_IsString(kt) || !kt->valuestring) { return false; } if (!hex_to_bin(kh->valuestring, key->key, 6)) { return false; } key->key_b = (strcmp(kt->valuestring, "B") == 0); if (key_hex_out && key_hex_cap > 0) { size_t len = strlen(kh->valuestring); if (len >= key_hex_cap) { return false; } memcpy(key_hex_out, kh->valuestring, len + 1); } return true; } static cJSON *build_type2_capture_locked(const nfc_tag_info_t *tag, int max_pages) { if (!tag) { return NULL; } if (max_pages < 4) { max_pages = 4; } if (max_pages > 240) { max_pages = 240; } cJSON *root = cJSON_CreateObject(); cJSON *tag_json = nfc_tag_to_json(tag); cJSON *pages = cJSON_CreateArray(); if (!root || !tag_json || !pages) { cJSON_Delete(root); cJSON_Delete(tag_json); cJSON_Delete(pages); return NULL; } cJSON_AddStringToObject(root, "format", "type2"); cJSON_AddItemToObject(root, "tag", tag_json); uint8_t version[8]; if (nfc_type2_get_version(version) == ESP_OK) { (void)add_hex_to_object(root, "getVersionHex", version, sizeof(version)); } for (int page = 0; page < max_pages; page++) { uint8_t data[4]; if (nfc_ultralight_read_page((uint8_t)page, data) != ESP_OK) { break; } char hx[9]; bin_to_hex(data, sizeof(data), hx, sizeof(hx)); cJSON_AddItemToArray(pages, cJSON_CreateString(hx)); } cJSON_AddNumberToObject(root, "pageCount", cJSON_GetArraySize(pages)); cJSON_AddItemToObject(root, "pagesHex", pages); cJSON_AddStringToObject(root, "programNote", "Programming skips pages 0-3 by default because they contain manufacturer and lock/config data."); return root; } static cJSON *build_classic_capture_locked(const nfc_tag_info_t *tag, const uint8_t *extra_keys, size_t extra_n, bool variations) { if (!tag) { return NULL; } int sectors = nfc_mifare_sector_count(tag); if (sectors <= 0) { return NULL; } cJSON *attack = nfc_mifare_dictionary_attack((nfc_tag_info_t *)tag, 0, (uint8_t)(sectors - 1), extra_keys, extra_n, variations, NULL); cJSON *root = cJSON_CreateObject(); cJSON *tag_json = nfc_tag_to_json(tag); cJSON *arr = cJSON_CreateArray(); if (!attack || !root || !tag_json || !arr) { cJSON_Delete(attack); cJSON_Delete(root); cJSON_Delete(tag_json); cJSON_Delete(arr); return NULL; } cJSON_AddStringToObject(root, "format", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic"); cJSON_AddItemToObject(root, "tag", tag_json); cJSON_AddNumberToObject(root, "sectorCount", sectors); cJSON_AddItemToObject(root, "keySearch", attack); for (int sector = 0; sector < sectors; sector++) { int first_block = 0; int block_count = 0; uint8_t trailer_block = 0; cJSON *sec = cJSON_CreateObject(); if (!sec) { cJSON_Delete(root); cJSON_Delete(arr); return NULL; } cJSON_AddNumberToObject(sec, "sector", sector); if (!nfc_mifare_sector_layout(tag, sector, &first_block, &block_count, &trailer_block)) { cJSON_AddBoolToObject(sec, "layoutError", true); cJSON_AddItemToArray(arr, sec); continue; } cJSON_AddNumberToObject(sec, "firstBlock", first_block); cJSON_AddNumberToObject(sec, "blockCount", block_count); cJSON_AddNumberToObject(sec, "trailerBlock", trailer_block); cJSON *hit = find_sector_hit(attack, sector); nfc_mifare_key_t key; char key_hex[13]; if (!parse_sector_key_hit(hit, &key, key_hex, sizeof(key_hex))) { cJSON_AddBoolToObject(sec, "authFailed", true); cJSON_AddItemToArray(arr, sec); continue; } cJSON_AddStringToObject(sec, "keyHex", key_hex); cJSON_AddStringToObject(sec, "keyType", key.key_b ? "B" : "A"); if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) { cJSON_AddBoolToObject(sec, "authFailed", true); cJSON_AddItemToArray(arr, sec); continue; } cJSON *blocks = cJSON_CreateArray(); if (!blocks) { cJSON_Delete(sec); cJSON_Delete(root); cJSON_Delete(arr); return NULL; } for (int i = 0; i < block_count; i++) { uint8_t block_data[NFC_BLOCK_LEN]; if (nfc_mifare_read_block((uint8_t)(first_block + i), block_data) == ESP_OK) { char hx[NFC_BLOCK_LEN * 2 + 1]; bin_to_hex(block_data, sizeof(block_data), hx, sizeof(hx)); cJSON_AddItemToArray(blocks, cJSON_CreateString(hx)); } else { cJSON_AddItemToArray(blocks, cJSON_CreateNull()); } } cJSON_AddItemToObject(sec, "blocksHex", blocks); cJSON_AddItemToArray(arr, sec); } cJSON_AddItemToObject(root, "sectors", arr); cJSON_AddStringToObject(root, "programNote", "Programming writes data blocks and optionally trailers; block 0 is always skipped on normal Classic cards."); return root; } static cJSON *program_type2_snapshot_locked(const cJSON *snapshot, bool include_lock_pages) { cJSON *pages = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "pagesHex"); if (!cJSON_IsArray(pages)) { return NULL; } int count = cJSON_GetArraySize(pages); int first_page = include_lock_pages ? 0 : 4; cJSON *out = cJSON_CreateObject(); if (!out) { return NULL; } int written = 0; int skipped = 0; int failed = 0; for (int page = 0; page < count; page++) { if (page < first_page) { skipped++; continue; } cJSON *it = cJSON_GetArrayItem(pages, page); if (!cJSON_IsString(it) || !it->valuestring) { skipped++; continue; } uint8_t data[4]; if (!hex_to_bin(it->valuestring, data, sizeof(data))) { failed++; continue; } if (nfc_ultralight_write_page((uint8_t)page, data) == ESP_OK) { written++; } else { failed++; } } cJSON_AddStringToObject(out, "format", "type2"); cJSON_AddNumberToObject(out, "writtenPages", written); cJSON_AddNumberToObject(out, "skippedPages", skipped); cJSON_AddNumberToObject(out, "failedPages", failed); cJSON_AddBoolToObject(out, "includeLockPages", include_lock_pages); return out; } static cJSON *program_classic_snapshot_locked(const nfc_tag_info_t *tag, const cJSON *snapshot, bool include_trailers) { if (!tag) { return NULL; } cJSON *sectors = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "sectors"); if (!cJSON_IsArray(sectors)) { return NULL; } int written = 0; int skipped = 0; int failed = 0; int sectors_n = cJSON_GetArraySize(sectors); for (int si = 0; si < sectors_n; si++) { cJSON *sec = cJSON_GetArrayItem(sectors, si); if (!sec) { skipped++; continue; } cJSON *blocks = cJSON_GetObjectItemCaseSensitive(sec, "blocksHex"); cJSON *first = cJSON_GetObjectItemCaseSensitive(sec, "firstBlock"); cJSON *count = cJSON_GetObjectItemCaseSensitive(sec, "blockCount"); cJSON *trailer = cJSON_GetObjectItemCaseSensitive(sec, "trailerBlock"); if (!cJSON_IsArray(blocks) || !cJSON_IsNumber(first) || !cJSON_IsNumber(count) || !cJSON_IsNumber(trailer)) { skipped++; continue; } nfc_mifare_key_t key; if (!parse_sector_key_hit(sec, &key, NULL, 0)) { failed++; continue; } uint8_t trailer_block = (uint8_t)cJSON_GetNumberValue(trailer); if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) { failed++; continue; } int first_block = (int)cJSON_GetNumberValue(first); int block_count = (int)cJSON_GetNumberValue(count); int blocks_n = cJSON_GetArraySize(blocks); int limit = block_count < blocks_n ? block_count : blocks_n; for (int bi = 0; bi < limit; bi++) { int block_no = first_block + bi; if (block_no == 0) { skipped++; continue; } if (block_no == trailer_block && !include_trailers) { skipped++; continue; } cJSON *it = cJSON_GetArrayItem(blocks, bi); if (!cJSON_IsString(it) || !it->valuestring) { skipped++; continue; } uint8_t data[NFC_BLOCK_LEN]; if (!hex_to_bin(it->valuestring, data, sizeof(data))) { failed++; continue; } if (nfc_mifare_write_block((uint8_t)block_no, data) == ESP_OK) { written++; } else { failed++; } } } cJSON *out = cJSON_CreateObject(); if (!out) { return NULL; } cJSON_AddStringToObject(out, "format", "mifareClassic"); cJSON_AddNumberToObject(out, "writtenBlocks", written); cJSON_AddNumberToObject(out, "skippedBlocks", skipped); cJSON_AddNumberToObject(out, "failedBlocks", failed); cJSON_AddBoolToObject(out, "includeTrailers", include_trailers); return out; } static esp_err_t api_status(httpd_req_t *req) { cJSON *o = cJSON_CreateObject(); if (!o) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory"); return ESP_ERR_NO_MEM; } 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); bool pn532_ok = nfc_engine_is_ready(); cJSON_AddBoolToObject(o, "pn532Connected", pn532_ok); if (pn532_ok) { uint8_t ic = 0, hi = 0, lo = 0; nfc_access_lock(); if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) { cJSON *pn = cJSON_CreateObject(); if (pn) { cJSON_AddNumberToObject(pn, "ic", ic); cJSON_AddNumberToObject(pn, "fwHi", hi); cJSON_AddNumberToObject(pn, "fwLo", lo); cJSON_AddItemToObject(o, "pn532", pn); } } nfc_access_unlock(); } cJSON_AddBoolToObject(o, "scanning", s_scan); cJSON_AddBoolToObject(o, "targetActive", s_target_active); 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(); if (!cap) { cJSON_Delete(o); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory"); return ESP_ERR_NO_MEM; } 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"), 500); } 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); } if (r == 0) { cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "deepCapture", session_capture_deep_enabled()); return send_json(req, o, 200); } 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) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char drain[128]; (void)recv_body_capped(req, drain, sizeof(drain)); nfc_tag_info_t tag; nfc_access_lock(); esp_err_t e = nfc_poll_passive_target(&tag); if (e == ESP_ERR_NOT_FOUND) { nfc_access_unlock(); cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "present", false); return send_json(req, o, 200); } if (e != ESP_OK) { nfc_access_unlock(); cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(e)); return send_json(req, o, 400); } cJSON *tj = nfc_tag_to_json(&tag); nfc_access_unlock(); if (!tj) { return send_json(req, cJSON_CreateString("out of memory building tag"), 500); } cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "present", true); cJSON_AddItemToObject(o, "tag", tj); 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); } if (r == 0) { cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "enable", s_scan); return send_json(req, o, 200); } 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) && cJSON_IsTrue(en) && s_target_active) { cJSON_Delete(j); return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409); } 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) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } uint8_t gs[32]; size_t gl = 0; nfc_access_lock(); esp_err_t e = pn532_get_general_status(gs, sizeof(gs), &gl); nfc_access_unlock(); 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(); if (!arr) { cJSON_Delete(o); return send_json(req, cJSON_CreateString("out of memory"), 500); } 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_nfc_probe(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char drain[64]; (void)recv_body_capped(req, drain, sizeof(drain)); nfc_tag_info_t tag; nfc_access_lock(); esp_err_t err = nfc_poll_passive_target(&tag); if (err != ESP_OK) { nfc_access_unlock(); cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", err == ESP_ERR_NOT_FOUND ? "no tag" : esp_err_to_name(err)); return send_json(req, o, err == ESP_ERR_NOT_FOUND ? 400 : 500); } cJSON *o = build_probe_locked(&tag); nfc_access_unlock(); if (!o) { return send_json(req, cJSON_CreateString("probe failed"), 500); } return send_json(req, o, 200); } static esp_err_t api_clone_capture(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char *body = NULL; cJSON *j = NULL; if (req->content_len > 0) { body = recv_body_alloc(req, 8192, NULL); if (!body) { return send_json(req, cJSON_CreateString("body too large"), 400); } j = cJSON_Parse(body); free(body); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } } else { j = cJSON_CreateObject(); } char mode[16] = "auto"; (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "mode"), mode, sizeof(mode)); int max_pages = json_get_int_default(j, "maxPages", 64); bool variations = json_get_bool_default(j, "variations", false); uint8_t extra[96 * 6]; size_t extra_n = 0; cJSON *keys = cJSON_GetObjectItemCaseSensitive(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) && it->valuestring && hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) { extra_n++; } } } cJSON_Delete(j); nfc_tag_info_t tag; nfc_access_lock(); esp_err_t err = nfc_poll_passive_target(&tag); if (err != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("no tag present"), 400); } cJSON *out = NULL; if (strcmp(mode, "classic") == 0) { if (!nfc_tag_is_mifare_classic(&tag)) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400); } out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations); } else if (strcmp(mode, "type2") == 0) { if (!nfc_tag_is_type2(&tag)) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not Type 2 / Ultralight"), 400); } out = build_type2_capture_locked(&tag, max_pages); } else if (nfc_tag_is_mifare_classic(&tag)) { out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations); } else if (nfc_tag_is_type2(&tag)) { out = build_type2_capture_locked(&tag, max_pages); } nfc_access_unlock(); if (!out) { return send_json(req, cJSON_CreateString("capture unsupported or failed"), 400); } return send_json(req, out, 200); } static esp_err_t api_clone_program(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char *body = recv_body_alloc(req, 65536, NULL); if (!body) { return send_json(req, cJSON_CreateString("snapshot body too large"), 400); } cJSON *j = cJSON_Parse(body); free(body); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } cJSON *snapshot = cJSON_GetObjectItemCaseSensitive(j, "snapshot"); cJSON *format = snapshot ? cJSON_GetObjectItemCaseSensitive(snapshot, "format") : NULL; bool include_trailers = json_get_bool_default(j, "includeTrailers", false); bool include_lock_pages = json_get_bool_default(j, "includeLockPages", false); if (!cJSON_IsObject(snapshot) || !cJSON_IsString(format) || !format->valuestring) { cJSON_Delete(j); return send_json(req, cJSON_CreateString("snapshot.format required"), 400); } nfc_tag_info_t tag; cJSON *out = NULL; nfc_access_lock(); if (strcmp(format->valuestring, "type2") == 0) { esp_err_t err = ensure_ultralight_tag_locked(&tag); if (err == ESP_OK) { out = program_type2_snapshot_locked(snapshot, include_lock_pages); } } else if (strncmp(format->valuestring, "mifareClassic", 13) == 0) { esp_err_t err = ensure_mifare_classic_tag_locked(&tag); if (err == ESP_OK) { out = program_classic_snapshot_locked(&tag, snapshot, include_trailers); } } nfc_access_unlock(); cJSON_Delete(j); if (!out) { return send_json(req, cJSON_CreateString("program failed or unsupported target"), 400); } return send_json(req, out, 200); } static esp_err_t api_mifare_read(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char buf[512]; char key_hex[13]; 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; cJSON *jb = cJSON_GetObjectItem(j, "keyB"); bool key_b = cJSON_IsTrue(jb); bool have_key = copy_json_string(jk, key_hex, sizeof(key_hex)); cJSON_Delete(j); if (block < 0 || block > 255 || !have_key) { return send_json(req, cJSON_CreateString("block/key required (block 0-255)"), 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; nfc_access_lock(); esp_err_t err = ensure_mifare_classic_tag_locked(&tag); if (err == ESP_ERR_NOT_SUPPORTED) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400); } if (err != ESP_OK) { nfc_access_unlock(); 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) { nfc_access_unlock(); 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) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("read failed"), 400); } nfc_access_unlock(); 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) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char buf[512]; char key_hex[13]; char data_hex[33]; 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; cJSON *jb = cJSON_GetObjectItem(j, "keyB"); bool key_b = cJSON_IsTrue(jb); bool have_key = copy_json_string(jk, key_hex, sizeof(key_hex)); bool have_data = copy_json_string(jd, data_hex, sizeof(data_hex)); cJSON_Delete(j); if (block < 0 || block > 255 || !have_key || !have_data) { return send_json(req, cJSON_CreateString("block/key/data required (block 0-255)"), 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; nfc_access_lock(); esp_err_t err = ensure_mifare_classic_tag_locked(&tag); if (err == ESP_ERR_NOT_SUPPORTED) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400); } if (err != ESP_OK) { nfc_access_unlock(); 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) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("auth failed"), 400); } if (nfc_mifare_write_block((uint8_t)block, blk) != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("write failed"), 400); } nfc_access_unlock(); return send_json(req, cJSON_CreateObject(), 200); } static esp_err_t api_ul_read(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } 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 || page > 255) { return send_json(req, cJSON_CreateString("page required (0-255)"), 400); } uint8_t d[4]; nfc_tag_info_t tag; nfc_access_lock(); esp_err_t e = ensure_ultralight_tag_locked(&tag); if (e == ESP_ERR_NOT_SUPPORTED) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not Ultralight/NTAG"), 400); } if (e != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("no tag"), 400); } if (nfc_ultralight_read_page((uint8_t)page, d) != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("read failed"), 400); } nfc_access_unlock(); 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) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char buf[128]; char data_hex[9]; 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; bool have_data = copy_json_string(jd, data_hex, sizeof(data_hex)); cJSON_Delete(j); if (page < 0 || page > 255 || !have_data) { return send_json(req, cJSON_CreateString("page and data (8 hex) required (page 0-255)"), 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); } nfc_tag_info_t tag; nfc_access_lock(); esp_err_t e = ensure_ultralight_tag_locked(&tag); if (e == ESP_ERR_NOT_SUPPORTED) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("tag is not Ultralight/NTAG"), 400); } if (e != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("no tag"), 400); } if (nfc_ultralight_write_page((uint8_t)page, d) != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("write failed"), 400); } nfc_access_unlock(); 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_target_status(httpd_req_t *req) { char drain[64]; (void)recv_body_capped(req, drain, sizeof(drain)); cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "active", s_target_active); return send_json(req, o, 200); } static esp_err_t api_target_start(httpd_req_t *req) { char *body = req->content_len ? recv_body_alloc(req, 4096, NULL) : NULL; cJSON *j = body ? cJSON_Parse(body) : cJSON_CreateObject(); free(body); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } char raw_params_hex[600] = {0}; char sens_res_hex[5] = "0400"; char nfcid1t_hex[7] = "112233"; char sel_res_hex[3] = "20"; char felica_hex[37] = "01FEA2A3A4A5A6A7C0C1C2C3C4C5C6C7FFFF"; char nfcid3t_hex[21] = "F0140102030405060708"; char gt_hex[256] = {0}; char tk_hex[256] = {0}; (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "rawParamsHex"), raw_params_hex, sizeof(raw_params_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "sensResHex"), sens_res_hex, sizeof(sens_res_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid1tHex"), nfcid1t_hex, sizeof(nfcid1t_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "selResHex"), sel_res_hex, sizeof(sel_res_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "felicaParamsHex"), felica_hex, sizeof(felica_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid3tHex"), nfcid3t_hex, sizeof(nfcid3t_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "generalTargetHex"), gt_hex, sizeof(gt_hex)); (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "historicalTargetHex"), tk_hex, sizeof(tk_hex)); int mode_byte = json_get_int_default(j, "modeByte", 0x04); int timeout_ms = json_get_int_default(j, "timeoutMs", 1000); cJSON_Delete(j); uint8_t params[256]; size_t params_len = 0; if (raw_params_hex[0]) { if (!hex_decode_flex(raw_params_hex, params, sizeof(params), ¶ms_len)) { return send_json(req, cJSON_CreateString("rawParamsHex invalid"), 400); } } else { uint8_t sens_res[2], nfcid1t[3], sel_res[1], felica[18], nfcid3t[10], gt[127], tk[127]; size_t gt_len = 0, tk_len = 0; if (!hex_to_bin(sens_res_hex, sens_res, sizeof(sens_res)) || !hex_to_bin(nfcid1t_hex, nfcid1t, sizeof(nfcid1t)) || !hex_to_bin(sel_res_hex, sel_res, sizeof(sel_res)) || !hex_to_bin(felica_hex, felica, sizeof(felica)) || !hex_to_bin(nfcid3t_hex, nfcid3t, sizeof(nfcid3t)) || (gt_hex[0] && !hex_decode_flex(gt_hex, gt, sizeof(gt), >_len)) || (tk_hex[0] && !hex_decode_flex(tk_hex, tk, sizeof(tk), &tk_len))) { return send_json(req, cJSON_CreateString("target parameter hex invalid"), 400); } params[params_len++] = (uint8_t)mode_byte; memcpy(params + params_len, sens_res, sizeof(sens_res)); params_len += sizeof(sens_res); memcpy(params + params_len, nfcid1t, sizeof(nfcid1t)); params_len += sizeof(nfcid1t); memcpy(params + params_len, sel_res, sizeof(sel_res)); params_len += sizeof(sel_res); memcpy(params + params_len, felica, sizeof(felica)); params_len += sizeof(felica); memcpy(params + params_len, nfcid3t, sizeof(nfcid3t)); params_len += sizeof(nfcid3t); params[params_len++] = (uint8_t)gt_len; if (gt_len) { memcpy(params + params_len, gt, gt_len); params_len += gt_len; } params[params_len++] = (uint8_t)tk_len; if (tk_len) { memcpy(params + params_len, tk, tk_len); params_len += tk_len; } } uint8_t resp[128]; size_t rlen = 0; bool prev_scan = s_scan; s_scan = false; nfc_access_lock(); esp_err_t err = pn532_tg_init_as_target(params, params_len, resp, sizeof(resp), &rlen, timeout_ms); nfc_access_unlock(); if (err != ESP_OK) { s_scan = prev_scan; cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 400); } s_target_active = true; cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "active", true); cJSON_AddNumberToObject(o, "modeByte", mode_byte); (void)add_hex_to_object(o, "responseHex", resp, rlen); return send_json(req, o, 200); } static esp_err_t api_target_recv(httpd_req_t *req) { char buf[128]; int timeout_ms = 15000; int r = recv_body_capped(req, buf, sizeof(buf)); if (r > 0) { cJSON *j = cJSON_Parse(buf); if (j) { timeout_ms = json_get_int_default(j, "timeoutMs", timeout_ms); cJSON_Delete(j); } } if (!s_target_active) { return send_json(req, cJSON_CreateString("target mode not active"), 409); } uint8_t resp[300]; size_t rlen = 0; nfc_access_lock(); esp_err_t err = pn532_tg_get_data(resp, sizeof(resp), &rlen, timeout_ms); nfc_access_unlock(); if (err != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 400); } cJSON *o = cJSON_CreateObject(); uint8_t status = rlen ? resp[0] : 0xFF; cJSON_AddBoolToObject(o, "ok", status == 0x00); cJSON_AddNumberToObject(o, "status", status); if (rlen > 1) { (void)add_hex_to_object(o, "dataHex", resp + 1, rlen - 1); } else { cJSON_AddStringToObject(o, "dataHex", ""); } return send_json(req, o, 200); } static esp_err_t api_target_send(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("body required"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } char data_hex[521] = {0}; (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "dataHex"), data_hex, sizeof(data_hex)); int timeout_ms = json_get_int_default(j, "timeoutMs", 1000); cJSON_Delete(j); if (!s_target_active) { return send_json(req, cJSON_CreateString("target mode not active"), 409); } uint8_t data[260]; size_t data_len = 0; if (!data_hex[0] || !hex_decode_flex(data_hex, data, sizeof(data), &data_len)) { return send_json(req, cJSON_CreateString("dataHex required"), 400); } uint8_t resp[64]; size_t rlen = 0; nfc_access_lock(); esp_err_t err = pn532_tg_set_data(data, data_len, resp, sizeof(resp), &rlen, timeout_ms); nfc_access_unlock(); if (err != ESP_OK) { cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 400); } cJSON *o = cJSON_CreateObject(); uint8_t status = rlen ? resp[0] : 0xFF; cJSON_AddBoolToObject(o, "ok", status == 0x00); cJSON_AddNumberToObject(o, "status", status); (void)add_hex_to_object(o, "responseHex", resp, rlen); return send_json(req, o, 200); } static esp_err_t api_target_stop(httpd_req_t *req) { char drain[64]; (void)recv_body_capped(req, drain, sizeof(drain)); nfc_access_lock(); esp_err_t err = pn532_sam_config_normal(); nfc_access_unlock(); s_target_active = false; s_scan = true; cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "active", false); if (err != ESP_OK) { cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 400); } return send_json(req, o, 200); } static esp_err_t api_ota_real(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("url required"), 400); } cJSON *j = cJSON_Parse(buf); if (!j) { return send_json(req, cJSON_CreateString("bad json"), 400); } char url[512] = {0}; (void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "url"), url, sizeof(url)); bool reboot = json_get_bool_default(j, "reboot", true); cJSON_Delete(j); if (!url[0] || strncmp(url, "https://", 8) != 0) { return send_json(req, cJSON_CreateString("https url required"), 400); } bool prev_scan = s_scan; s_scan = false; esp_http_client_config_t http_cfg = { .url = url, .crt_bundle_attach = esp_crt_bundle_attach, .keep_alive_enable = true, .timeout_ms = 120000, }; esp_https_ota_config_t ota_cfg = { .http_config = &http_cfg, }; esp_err_t err = esp_https_ota(&ota_cfg); if (err != ESP_OK) { s_scan = prev_scan; cJSON *o = cJSON_CreateObject(); cJSON_AddStringToObject(o, "error", esp_err_to_name(err)); return send_json(req, o, 500); } cJSON *o = cJSON_CreateObject(); cJSON_AddBoolToObject(o, "ok", true); cJSON_AddBoolToObject(o, "rebooting", reboot); esp_err_t send_err = send_json(req, o, 200); if (send_err == ESP_OK && reboot) { vTaskDelay(pdMS_TO_TICKS(500)); esp_restart(); } return send_err; } static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } 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; nfc_access_lock(); if (nfc_poll_passive_target(&tag) != ESP_OK) { nfc_access_unlock(); return send_json(req, cJSON_CreateString("no tag present"), 400); } cJSON *out = nfc_mifare_dictionary_attack(&tag, s0, s1, extra_n ? extra : NULL, extra_n, variations, NULL); nfc_access_unlock(); if (!out) { return send_json(req, cJSON_CreateString("attack failed"), 400); } return send_json(req, out, 200); } static esp_err_t api_nfc_emulate_raw(httpd_req_t *req) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char buf[1024]; char hex[521]; 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"); bool have_hex = copy_json_string(jh, hex, sizeof(hex)); cJSON_Delete(j); uint8_t bin[260]; size_t blen = 0; if (!have_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; nfc_access_lock(); esp_err_t err = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 800); nfc_access_unlock(); 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); if (!rh) { return send_json(req, cJSON_CreateString("out of memory"), 500); } 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) { esp_err_t gate = reject_if_target_active(req); if (gate != ESP_OK) { return gate; } char buf[1024]; char hex[521]; 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"); bool have_hex = copy_json_string(jf, hex, sizeof(hex)); cJSON_Delete(j); size_t blen = 0; uint8_t bin[260]; if (!have_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; nfc_access_lock(); esp_err_t e = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 300); nfc_access_unlock(); 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); if (!rh) { return send_json(req, cJSON_CreateString("out of memory"), 500); } 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) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 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"); 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[STATIC_PATH_MAX]; int path_len = snprintf(path, sizeof path, "/spiffs%s", req->uri); if (path_len < 0 || path_len >= (int)sizeof(path)) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "path too long"); return ESP_FAIL; } 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"); } else if (strstr(req->uri, ".css")) { httpd_resp_set_type(req, "text/css"); } else if (strstr(req->uri, ".html")) { httpd_resp_set_type(req, "text/html"); } 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 + 1 < 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 reconnect_task(void *arg) { (void)arg; while (1) { vTaskDelay(pdMS_TO_TICKS(5000)); if (!nfc_engine_is_ready()) { nfc_access_lock(); bool ok = nfc_engine_try_reattach(); nfc_access_unlock(); if (ok) { ESP_LOGI(TAG, "PN532 reattached — broadcasting status"); app_net_broadcast_json("pn532", "{\"connected\":true}"); } } } } static void scan_loop_task(void *arg) { (void)arg; nfc_tag_info_t last; memset(&last, 0, sizeof(last)); while (1) { if (!nfc_engine_is_ready()) { vTaskDelay(pdMS_TO_TICKS(500)); continue; } 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; nfc_access_lock(); 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); if (j) { 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); nfc_access_unlock(); 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 { nfc_access_unlock(); } } else { nfc_access_unlock(); } } else { nfc_access_unlock(); 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 bool register_uri_checked(httpd_handle_t server, const httpd_uri_t *uri) { if (!server || !uri) { return false; } esp_err_t err = httpd_register_uri_handler(server, uri); if (err != ESP_OK) { ESP_LOGE(TAG, "register %s %s failed: %s", uri->method == HTTP_GET ? "GET" : uri->method == HTTP_POST ? "POST" : uri->method == HTTP_OPTIONS ? "OPTIONS" : "?", uri->uri ? uri->uri : "(null)", esp_err_to_name(err)); return false; } return true; } 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}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/poll", .method = HTTP_POST, .handler = api_nfc_poll}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/probe", .method = HTTP_POST, .handler = api_nfc_probe}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/target/status", .method = HTTP_POST, .handler = api_target_status}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/target/start", .method = HTTP_POST, .handler = api_target_start}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/target/recv", .method = HTTP_POST, .handler = api_target_recv}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/target/send", .method = HTTP_POST, .handler = api_target_send}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/target/stop", .method = HTTP_POST, .handler = api_target_stop}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/mifare/read-block", .method = HTTP_POST, .handler = api_mifare_read}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/mifare/write-block", .method = HTTP_POST, .handler = api_mifare_write}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/ul/read-page", .method = HTTP_POST, .handler = api_ul_read}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/ul/write-page", .method = HTTP_POST, .handler = api_ul_write}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/raw/pn532", .method = HTTP_POST, .handler = api_raw_pn532}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/clone/capture", .method = HTTP_POST, .handler = api_clone_capture}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/clone/program", .method = HTTP_POST, .handler = api_clone_program}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_real}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/session/export", .method = HTTP_GET, .handler = api_session_export}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/session/clear", .method = HTTP_POST, .handler = api_session_clear}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/session/deep", .method = HTTP_POST, .handler = api_session_deep}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/mifare/dictionary-attack", .method = HTTP_POST, .handler = api_mifare_dictionary_attack}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/api/nfc/emulate-raw", .method = HTTP_POST, .handler = api_nfc_emulate_raw}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } u = (httpd_uri_t){.uri = "/*", .method = HTTP_GET, .handler = static_any}; if (!register_uri_checked(s, &u)) { httpd_stop(s); return NULL; } return s; } esp_err_t app_net_init(void) { esp_err_t err = nvs_flash_init(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { err = nvs_flash_erase(); if (err != ESP_OK) { return err; } err = nvs_flash_init(); } if (err != ESP_OK) { return err; } err = esp_netif_init(); if (err != ESP_OK) { return err; } err = esp_event_loop_create_default(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { return err; } esp_netif_t *ap_if = esp_netif_create_default_wifi_ap(); if (!ap_if) { return ESP_FAIL; } s_nfc_op_mu = xSemaphoreCreateMutex(); if (!s_nfc_op_mu) { return ESP_ERR_NO_MEM; } wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT(); err = esp_wifi_init(&wcfg); if (err != ESP_OK) { return err; } 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; err = esp_wifi_set_mode(WIFI_MODE_AP); if (err != ESP_OK) { return err; } err = esp_wifi_set_config(WIFI_IF_AP, &ap); if (err != ESP_OK) { return err; } err = esp_wifi_start(); if (err != ESP_OK) { return err; } esp_vfs_spiffs_conf_t sp = { .base_path = "/spiffs", .partition_label = "storage", .max_files = 16, .format_if_mount_failed = true, }; err = esp_vfs_spiffs_register(&sp); if (err != ESP_OK) { return err; } err = mdns_init(); if (err == ESP_OK) { (void)mdns_hostname_set("pn532tool"); (void)mdns_instance_name_set("PN532 NFC Toolkit"); (void)mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); } else { ESP_LOGW(TAG, "mDNS init failed: %s", esp_err_to_name(err)); } s_server = start_server(); if (!s_server) { return ESP_FAIL; } if (xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task) != pdPASS) { httpd_stop(s_server); s_server = NULL; return ESP_ERR_NO_MEM; } if (xTaskCreate(reconnect_task, "pn532_reconnect", 4096, NULL, 3, NULL) != pdPASS) { ESP_LOGW(TAG, "reconnect task create failed — hot-plug retry disabled"); } return ESP_OK; }