Files
nfc-pn532-warlord/firmware/components/net_service/app_net.c
drjones 3be2d3f936 Fix PN532 transport and NFC tag handling.
Tighten PN532 framing and NFC access locking so scan, read, write, and raw operations behave reliably on hardware, and correct Classic versus Type 2 tag classification so common cards hit the right code paths. Refresh the embedded web assets and launcher files to match the corrected firmware behavior.

Made-with: Cursor
2026-04-02 18:32:06 -07:00

1194 lines
38 KiB
C

#include "net_service/app_net.h"
#include "esp_event.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/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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/stat.h>
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 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;
}
/** 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_frame_async(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 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);
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();
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);
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)
{
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)) {
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;
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_mifare_read(httpd_req_t *req)
{
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)
{
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)
{
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)
{
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_ota_stub(httpd_req_t *req)
{
cJSON *o = cJSON_CreateString("Use idf.py app-flash or extend with esp_https_ota + bundle URL");
return send_json(req, o, 501);
}
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;
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)
{
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)
{
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, "<h1>PN532 Toolkit</h1><p>Build web UI into /data</p>", 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 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;
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/scan", .method = HTTP_POST, .handler = api_scan};
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/ota", .method = HTTP_POST, .handler = api_ota_stub};
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) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}