Files
nfc-pn532-warlord/firmware/components/net_service/app_net.c
drjones 8968560565 Add PN532 toolkit firmware, web UI, and embedded SPIFFS assets
Includes ESP-IDF NFC stack (deep capture, 4K Classic geometry, UL write API,
open SoftAP), React dashboard with live tag diagnostics, and docs. README
updated for APIs and lab Wi-Fi defaults.

Made-with: Cursor
2026-03-29 09:32:55 -07:00

909 lines
31 KiB
C

#include "net_service/app_net.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include "esp_spiffs.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mdns.h"
#include "nfc_engine/nfc_brute.h"
#include "nfc_engine/nfc_deep.h"
#include "nfc_engine/nfc_engine.h"
#include "nfc_engine/session_capture.h"
#include "nvs_flash.h"
#include "pn532_host/pn532_core.h"
#include "cJSON.h"
#include "http_parser.h"
#include <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 MAX_JSON 4096
#define WS_BROADCAST_BUF 4096
static httpd_handle_t s_server;
static bool s_scan = true;
static TaskHandle_t s_scan_task;
static int hexval(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return 10 + c - 'a';
}
if (c >= 'A' && c <= 'F') {
return 10 + c - 'A';
}
return -1;
}
static bool hex_to_bin(const char *hex, uint8_t *out, size_t out_len)
{
size_t n = strlen(hex);
if (n != out_len * 2) {
return false;
}
for (size_t i = 0; i < out_len; i++) {
int h = hexval(hex[i * 2]);
int l = hexval(hex[i * 2 + 1]);
if (h < 0 || l < 0) {
return false;
}
out[i] = (uint8_t)((h << 4) | l);
}
return true;
}
static bool hex_decode_flex(const char *hex, uint8_t *out, size_t out_cap, size_t *out_len)
{
size_t n = strlen(hex);
if (n % 2 || n / 2 > out_cap) {
return false;
}
*out_len = n / 2;
return hex_to_bin(hex, out, *out_len);
}
/** Read full POST body (httpd may return partial data in multiple recv calls). */
static int recv_body_capped(httpd_req_t *req, char *buf, size_t cap)
{
if (!buf || cap < 2) {
return -1;
}
size_t cl = req->content_len;
if (cl >= cap) {
return -1;
}
if (cl == 0) {
buf[0] = '\0';
return 0;
}
size_t got = 0;
while (got < cl) {
int r = httpd_req_recv(req, buf + got, cl - got);
if (r <= 0) {
return -1;
}
got += (size_t)r;
}
buf[got] = '\0';
return (int)got;
}
static char *recv_body_alloc(httpd_req_t *req, size_t max_len, int *out_len)
{
size_t cl = req->content_len;
if (cl == 0 || cl > max_len) {
return NULL;
}
char *buf = malloc(cl + 1);
if (!buf) {
return NULL;
}
size_t got = 0;
while (got < cl) {
int r = httpd_req_recv(req, buf + got, cl - got);
if (r <= 0) {
free(buf);
return NULL;
}
got += (size_t)r;
}
buf[cl] = '\0';
if (out_len) {
*out_len = (int)cl;
}
return buf;
}
esp_err_t app_net_broadcast_json(const char *channel, const char *json_text)
{
(void)channel;
if (!s_server || !json_text) {
return ESP_ERR_INVALID_STATE;
}
char line[WS_BROADCAST_BUF];
int n = snprintf(line, sizeof(line), "{\"channel\":\"%s\",\"payload\":%s}", channel ? channel : "event",
json_text);
if (n < 0 || n >= (int)sizeof(line)) {
return ESP_ERR_NO_MEM;
}
int fds[16];
size_t fdcount = sizeof(fds) / sizeof(fds[0]);
esp_err_t er = httpd_get_client_list(s_server, &fdcount, fds);
if (er != ESP_OK) {
return er;
}
httpd_ws_frame_t pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)line, .len = (size_t)n};
for (size_t i = 0; i < fdcount; i++) {
if (httpd_ws_get_fd_info(s_server, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
(void)httpd_ws_send_frame_async(s_server, fds[i], &pkt);
}
}
return ESP_OK;
}
static esp_err_t send_json(httpd_req_t *req, cJSON *j, int status)
{
char *p = cJSON_PrintUnformatted(j);
cJSON_Delete(j);
if (!p) {
return ESP_ERR_NO_MEM;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
httpd_resp_set_status(req, status == 200 ? "200 OK" : "400 Bad Request");
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
free(p);
return e;
}
static esp_err_t api_status(httpd_req_t *req)
{
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "app", "pn532_nfc_toolkit");
cJSON_AddNumberToObject(o, "uptimeMs", (double)(esp_timer_get_time() / 1000));
cJSON_AddNumberToObject(o, "freeHeap", (double)esp_get_free_heap_size());
wifi_mode_t mode;
esp_wifi_get_mode(&mode);
cJSON_AddNumberToObject(o, "wifiMode", mode);
uint8_t ic = 0, hi = 0, lo = 0;
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
cJSON *pn = cJSON_CreateObject();
cJSON_AddNumberToObject(pn, "ic", ic);
cJSON_AddNumberToObject(pn, "fwHi", hi);
cJSON_AddNumberToObject(pn, "fwLo", lo);
cJSON_AddItemToObject(o, "pn532", pn);
}
cJSON_AddBoolToObject(o, "scanning", s_scan);
size_t cap_u = 0;
uint32_t cap_l = 0;
bool cap_f = false;
session_capture_get_status(&cap_u, &cap_l, &cap_f);
cJSON *cap = cJSON_CreateObject();
cJSON_AddNumberToObject(cap, "usedBytes", (double)cap_u);
cJSON_AddNumberToObject(cap, "maxBytes", (double)session_capture_max());
cJSON_AddNumberToObject(cap, "lines", (double)cap_l);
cJSON_AddBoolToObject(cap, "full", cap_f);
cJSON_AddBoolToObject(cap, "deepCapture", session_capture_deep_enabled());
cJSON_AddItemToObject(o, "session", cap);
return send_json(req, o, 200);
}
static void tag_uid_hex(const nfc_tag_info_t *tag, char *out, size_t out_sz)
{
if (!out || out_sz == 0) {
return;
}
out[0] = 0;
size_t p = 0;
for (int i = 0; i < tag->uid_len && p + 2 < out_sz; i++) {
p += (size_t)snprintf(out + p, out_sz - p, "%02X", tag->uid[i]);
}
}
static esp_err_t api_session_export(httpd_req_t *req)
{
size_t n = session_capture_export_size();
if (n == 0) {
httpd_resp_set_type(req, "text/plain");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
return httpd_resp_send(req, "empty", HTTPD_RESP_USE_STRLEN);
}
char *buf = malloc(n);
if (!buf) {
return send_json(req, cJSON_CreateString("out of memory"), 400);
}
size_t got = 0;
session_capture_copy_to(buf, n, &got);
httpd_resp_set_type(req, "application/x-ndjson");
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"pn532-deep-capture.ndjson\"");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
esp_err_t er = httpd_resp_send(req, buf, got);
free(buf);
return er;
}
static esp_err_t api_session_clear(httpd_req_t *req)
{
char drain[128];
(void)recv_body_capped(req, drain, sizeof(drain));
session_capture_clear();
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "ok", true);
return send_json(req, o, 200);
}
static esp_err_t api_session_deep(httpd_req_t *req)
{
char buf[128];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("body required or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *en = cJSON_GetObjectItem(j, "enable");
if (cJSON_IsBool(en)) {
session_capture_set_deep(cJSON_IsTrue(en));
}
cJSON_Delete(j);
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "deepCapture", session_capture_deep_enabled());
return send_json(req, o, 200);
}
static esp_err_t api_nfc_poll(httpd_req_t *req)
{
char drain[128];
(void)recv_body_capped(req, drain, sizeof(drain));
nfc_tag_info_t tag;
esp_err_t e = nfc_poll_passive_target(&tag);
if (e == ESP_ERR_NOT_FOUND) {
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "present", false);
return send_json(req, o, 200);
}
if (e != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
return send_json(req, o, 400);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "present", true);
cJSON_AddItemToObject(o, "tag", nfc_tag_to_json(&tag));
return send_json(req, o, 200);
}
static esp_err_t api_scan(httpd_req_t *req)
{
char buf[128];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *en = cJSON_GetObjectItem(j, "enable");
if (cJSON_IsBool(en)) {
app_net_set_continuous_scan(cJSON_IsTrue(en));
}
cJSON_Delete(j);
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "enable", s_scan);
return send_json(req, o, 200);
}
static esp_err_t api_general_status(httpd_req_t *req)
{
uint8_t gs[32];
size_t gl = 0;
esp_err_t e = pn532_get_general_status(gs, sizeof(gs), &gl);
if (e != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
return send_json(req, o, 400);
}
cJSON *o = cJSON_CreateObject();
cJSON *arr = cJSON_CreateArray();
for (size_t i = 0; i < gl; i++) {
cJSON_AddItemToArray(arr, cJSON_CreateNumber(gs[i]));
}
cJSON_AddItemToObject(o, "raw", arr);
return send_json(req, o, 200);
}
static esp_err_t api_mifare_read(httpd_req_t *req)
{
char buf[512];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jbk = cJSON_GetObjectItem(j, "block");
cJSON *jk = cJSON_GetObjectItem(j, "key");
int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1;
const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL;
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
bool key_b = cJSON_IsTrue(jb);
cJSON_Delete(j);
if (block < 0 || !key_hex) {
return send_json(req, cJSON_CreateString("block/key required"), 400);
}
uint8_t keyb[6];
if (!hex_to_bin(key_hex, keyb, 6)) {
return send_json(req, cJSON_CreateString("key must be 12 hex chars"), 400);
}
nfc_tag_info_t tag;
if (nfc_poll_passive_target(&tag) != ESP_OK) {
return send_json(req, cJSON_CreateString("no tag"), 400);
}
nfc_mifare_key_t k = {.key_b = key_b};
memcpy(k.key, keyb, 6);
if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) {
return send_json(req, cJSON_CreateString("auth failed"), 400);
}
uint8_t blk[NFC_BLOCK_LEN];
if (nfc_mifare_read_block((uint8_t)block, blk) != ESP_OK) {
return send_json(req, cJSON_CreateString("read failed"), 400);
}
char hexout[NFC_BLOCK_LEN * 2 + 1];
for (int i = 0; i < NFC_BLOCK_LEN; i++) {
snprintf(hexout + i * 2, 3, "%02X", blk[i]);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddNumberToObject(o, "block", block);
cJSON_AddStringToObject(o, "data", hexout);
return send_json(req, o, 200);
}
static esp_err_t api_mifare_write(httpd_req_t *req)
{
char buf[512];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jbk = cJSON_GetObjectItem(j, "block");
cJSON *jk = cJSON_GetObjectItem(j, "key");
cJSON *jd = cJSON_GetObjectItem(j, "data");
int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1;
const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL;
const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL;
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
bool key_b = cJSON_IsTrue(jb);
cJSON_Delete(j);
if (block < 0 || !key_hex || !data_hex) {
return send_json(req, cJSON_CreateString("block/key/data required"), 400);
}
uint8_t keyb[6], blk[NFC_BLOCK_LEN];
if (!hex_to_bin(key_hex, keyb, 6) || !hex_to_bin(data_hex, blk, NFC_BLOCK_LEN)) {
return send_json(req, cJSON_CreateString("bad hex"), 400);
}
nfc_tag_info_t tag;
if (nfc_poll_passive_target(&tag) != ESP_OK) {
return send_json(req, cJSON_CreateString("no tag"), 400);
}
nfc_mifare_key_t k = {.key_b = key_b};
memcpy(k.key, keyb, 6);
if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) {
return send_json(req, cJSON_CreateString("auth failed"), 400);
}
if (nfc_mifare_write_block((uint8_t)block, blk) != ESP_OK) {
return send_json(req, cJSON_CreateString("write failed"), 400);
}
return send_json(req, cJSON_CreateObject(), 200);
}
static esp_err_t api_ul_read(httpd_req_t *req)
{
char buf[128];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jp = cJSON_GetObjectItem(j, "page");
int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1;
cJSON_Delete(j);
if (page < 0) {
return send_json(req, cJSON_CreateString("page required"), 400);
}
uint8_t d[4];
if (nfc_ultralight_read_page((uint8_t)page, d) != ESP_OK) {
return send_json(req, cJSON_CreateString("read failed"), 400);
}
char hx[9];
snprintf(hx, sizeof hx, "%02X%02X%02X%02X", d[0], d[1], d[2], d[3]);
cJSON *o = cJSON_CreateObject();
cJSON_AddNumberToObject(o, "page", page);
cJSON_AddStringToObject(o, "data", hx);
return send_json(req, o, 200);
}
static esp_err_t api_ul_write(httpd_req_t *req)
{
char buf[128];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jp = cJSON_GetObjectItem(j, "page");
cJSON *jd = cJSON_GetObjectItem(j, "data");
int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1;
const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL;
cJSON_Delete(j);
if (page < 0 || !data_hex) {
return send_json(req, cJSON_CreateString("page and data (8 hex) required"), 400);
}
uint8_t d[4];
if (!hex_to_bin(data_hex, d, 4)) {
return send_json(req, cJSON_CreateString("data must be 8 hex chars (4 bytes)"), 400);
}
if (nfc_ultralight_write_page((uint8_t)page, d) != ESP_OK) {
return send_json(req, cJSON_CreateString("write failed"), 400);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddNumberToObject(o, "page", page);
cJSON_AddBoolToObject(o, "ok", true);
return send_json(req, o, 200);
}
static esp_err_t api_ota_stub(httpd_req_t *req)
{
(void)req;
cJSON *o = cJSON_CreateString("Use idf.py app-flash or extend with esp_https_ota + bundle URL");
char *p = cJSON_PrintUnformatted(o);
cJSON_Delete(o);
if (!p) {
return ESP_ERR_NO_MEM;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_status(req, "501 Not Implemented");
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
free(p);
return e;
}
static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req)
{
char *body = recv_body_alloc(req, 8192, NULL);
if (!body) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(body);
free(body);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jr = cJSON_GetObjectItem(j, "readerType");
const char *rtype = cJSON_IsString(jr) ? jr->valuestring : NULL;
uint8_t s0 = 0;
uint8_t s1 = 15;
cJSON *jf = cJSON_GetObjectItem(j, "sectorFirst");
cJSON *jl = cJSON_GetObjectItem(j, "sectorLast");
if (cJSON_IsNumber(jf)) {
s0 = (uint8_t)cJSON_GetNumberValue(jf);
}
if (cJSON_IsNumber(jl)) {
s1 = (uint8_t)cJSON_GetNumberValue(jl);
} else if (rtype && strcmp(rtype, "classic4k") == 0) {
s1 = 39;
}
bool variations = cJSON_IsTrue(cJSON_GetObjectItem(j, "variations"));
uint8_t extra[96 * 6];
size_t extra_n = 0;
cJSON *keys = cJSON_GetObjectItem(j, "keysHex");
if (cJSON_IsArray(keys)) {
int n = cJSON_GetArraySize(keys);
for (int i = 0; i < n && extra_n < 96; i++) {
cJSON *it = cJSON_GetArrayItem(keys, i);
if (!cJSON_IsString(it)) {
continue;
}
if (hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) {
extra_n++;
}
}
}
cJSON_Delete(j);
nfc_tag_info_t tag;
if (nfc_poll_passive_target(&tag) != ESP_OK) {
return send_json(req, cJSON_CreateString("no tag present"), 400);
}
int attempts = 0;
cJSON *out = nfc_mifare_dictionary_attack(&tag, s0, s1, extra_n ? extra : NULL, extra_n, variations, &attempts);
if (!out) {
return send_json(req, cJSON_CreateString("attack failed"), 400);
}
char *p = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!p) {
return send_json(req, cJSON_CreateString("print failed"), 400);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_status(req, "200 OK");
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
free(p);
return e;
}
static esp_err_t api_nfc_emulate_raw(httpd_req_t *req)
{
char buf[1024];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jh = cJSON_GetObjectItem(j, "hex");
const char *hex = cJSON_IsString(jh) ? jh->valuestring : NULL;
cJSON_Delete(j);
uint8_t bin[260];
size_t blen = 0;
if (!hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) {
return send_json(req, cJSON_CreateString("hex command required"), 400);
}
uint8_t resp[300];
size_t rlen = 0;
esp_err_t err = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 800);
if (err != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 400);
}
char *rh = calloc(1, rlen * 2 + 1);
for (size_t i = 0; i < rlen; i++) {
snprintf(rh + i * 2, 3, "%02X", resp[i]);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "response", rh);
free(rh);
return send_json(req, o, 200);
}
static esp_err_t api_raw_pn532(httpd_req_t *req)
{
char buf[1024];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *jf = cJSON_GetObjectItem(j, "frame");
const char *hex = cJSON_IsString(jf) ? jf->valuestring : NULL;
cJSON_Delete(j);
size_t blen = 0;
uint8_t bin[260];
if (!hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) {
return send_json(req, cJSON_CreateString("frame hex required, even length, max 260B"), 400);
}
uint8_t resp[300];
size_t rlen = 0;
esp_err_t e = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 300);
if (e != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
return send_json(req, o, 400);
}
char *rh = calloc(1, rlen * 2 + 1);
for (size_t i = 0; i < rlen; i++) {
snprintf(rh + i * 2, 3, "%02X", resp[i]);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "response", rh);
free(rh);
return send_json(req, o, 200);
}
static esp_err_t api_cors_preflight(httpd_req_t *req)
{
(void)req;
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "86400");
httpd_resp_set_status(req, "204 No Content");
return httpd_resp_send(req, "", 0);
}
static esp_err_t static_any(httpd_req_t *req)
{
if (strcmp(req->uri, "/") == 0) {
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
FILE *f = fopen("/spiffs/index.html", "r");
if (!f) {
httpd_resp_send(req, "<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");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) {
fclose(f);
return ESP_FAIL;
}
}
fclose(f);
return httpd_resp_send_chunk(req, NULL, 0);
}
if (strstr(req->uri, "..") != NULL) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad path");
return ESP_FAIL;
}
char path[96];
snprintf(path, sizeof path, "/spiffs%s", req->uri);
struct stat st;
if (stat(path, &st) != 0) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found");
return ESP_FAIL;
}
FILE *f = fopen(path, "r");
if (!f) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found");
return ESP_FAIL;
}
if (strstr(req->uri, ".js")) {
httpd_resp_set_type(req, "application/javascript");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
} else if (strstr(req->uri, ".css")) {
httpd_resp_set_type(req, "text/css");
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
}
char buf[512];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) {
fclose(f);
return ESP_FAIL;
}
}
fclose(f);
return httpd_resp_send_chunk(req, NULL, 0);
}
static esp_err_t ws_handler(httpd_req_t *req)
{
if (req->method == HTTP_GET) {
ESP_LOGI(TAG, "WS handshake");
return ESP_OK;
}
httpd_ws_frame_t ws = {.type = HTTPD_WS_TYPE_TEXT};
uint8_t buf[128];
ws.payload = buf;
esp_err_t ret = httpd_ws_recv_frame(req, &ws, sizeof(buf));
if (ret != ESP_OK) {
return ret;
}
if (ws.type == HTTPD_WS_TYPE_TEXT && ws.len < sizeof(buf)) {
buf[ws.len] = 0;
if (strcmp((char *)buf, "ping") == 0) {
ws.type = HTTPD_WS_TYPE_TEXT;
ws.payload = (uint8_t *)"{\"channel\":\"pong\"}";
ws.len = strlen((char *)ws.payload);
return httpd_ws_send_frame(req, &ws);
}
}
return ESP_OK;
}
static void scan_loop_task(void *arg)
{
(void)arg;
nfc_tag_info_t last;
memset(&last, 0, sizeof(last));
while (1) {
if (!s_scan) {
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
if (session_capture_deep_enabled() && session_capture_is_full()) {
vTaskDelay(pdMS_TO_TICKS(400));
continue;
}
nfc_tag_info_t tag;
esp_err_t e = nfc_poll_passive_target(&tag);
if (e == ESP_OK) {
bool is_new = (last.uid_len != tag.uid_len || memcmp(last.uid, tag.uid, tag.uid_len) != 0);
if (is_new) {
last = tag;
cJSON *j = nfc_tag_to_json(&tag);
char *raw = cJSON_PrintUnformatted(j);
cJSON_Delete(j);
if (raw) {
app_net_broadcast_json("scan", raw);
free(raw);
}
if (session_capture_deep_enabled() && !session_capture_is_full()) {
cJSON *deep = nfc_tag_deep_profile(&tag);
char *line = deep ? cJSON_PrintUnformatted(deep) : NULL;
cJSON_Delete(deep);
if (line) {
if (!session_capture_append_line(line)) {
cJSON *mini = cJSON_CreateObject();
cJSON_AddStringToObject(mini, "event", "bufferFull");
cJSON_AddBoolToObject(mini, "paused", true);
char *m = cJSON_PrintUnformatted(mini);
cJSON_Delete(mini);
if (m) {
app_net_broadcast_json("capture", m);
free(m);
}
} else {
size_t u = 0;
uint32_t lc = 0;
bool f = false;
session_capture_get_status(&u, &lc, &f);
char uh[32];
tag_uid_hex(&tag, uh, sizeof uh);
cJSON *ev = cJSON_CreateObject();
cJSON_AddStringToObject(ev, "event", "recorded");
cJSON_AddStringToObject(ev, "uid", uh);
cJSON_AddNumberToObject(ev, "usedBytes", (double)u);
cJSON_AddNumberToObject(ev, "lines", (double)lc);
cJSON_AddBoolToObject(ev, "full", f);
char *es = cJSON_PrintUnformatted(ev);
cJSON_Delete(ev);
if (es) {
app_net_broadcast_json("capture", es);
free(es);
}
}
free(line);
}
}
}
} else {
if (last.uid_len) {
memset(&last, 0, sizeof(last));
app_net_broadcast_json("scan", "{\"present\":false}");
}
}
vTaskDelay(pdMS_TO_TICKS(session_capture_deep_enabled() ? 220 : 65));
}
}
void app_net_set_continuous_scan(bool on) { s_scan = on; }
bool app_net_continuous_scan(void) { return s_scan; }
static httpd_handle_t start_server(void)
{
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.lru_purge_enable = true;
cfg.max_uri_handlers = 48;
cfg.stack_size = 8192;
httpd_handle_t s = NULL;
if (httpd_start(&s, &cfg) != ESP_OK) {
return NULL;
}
httpd_uri_t u = {.uri = "/*", .method = HTTP_OPTIONS, .handler = api_cors_preflight};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/nfc/poll", .method = HTTP_POST, .handler = api_nfc_poll};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/mifare/read-block", .method = HTTP_POST, .handler = api_mifare_read};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/mifare/write-block", .method = HTTP_POST, .handler = api_mifare_write};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/ul/read-page", .method = HTTP_POST, .handler = api_ul_read};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/ul/write-page", .method = HTTP_POST, .handler = api_ul_write};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/raw/pn532", .method = HTTP_POST, .handler = api_raw_pn532};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_stub};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/session/export", .method = HTTP_GET, .handler = api_session_export};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/session/clear", .method = HTTP_POST, .handler = api_session_clear};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/session/deep", .method = HTTP_POST, .handler = api_session_deep};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/mifare/dictionary-attack", .method = HTTP_POST,
.handler = api_mifare_dictionary_attack};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/api/nfc/emulate-raw", .method = HTTP_POST, .handler = api_nfc_emulate_raw};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true};
httpd_register_uri_handler(s, &u);
u = (httpd_uri_t){.uri = "/*", .method = HTTP_GET, .handler = static_any};
httpd_register_uri_handler(s, &u);
return s;
}
esp_err_t app_net_init(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_ap();
wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wcfg));
wifi_config_t ap = {0};
strncpy((char *)ap.ap.ssid, SOFTAP_SSID, sizeof(ap.ap.ssid));
ap.ap.ssid_len = (uint8_t)strlen(SOFTAP_SSID);
ap.ap.channel = 6;
ap.ap.max_connection = 8;
ap.ap.authmode = WIFI_AUTH_OPEN;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap));
ESP_ERROR_CHECK(esp_wifi_start());
esp_vfs_spiffs_conf_t sp = {
.base_path = "/spiffs",
.partition_label = "storage",
.max_files = 16,
.format_if_mount_failed = true,
};
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&sp));
mdns_init();
mdns_hostname_set("pn532tool");
mdns_instance_name_set("PN532 NFC Toolkit");
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
s_server = start_server();
if (!s_server) {
return ESP_FAIL;
}
xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task);
return ESP_OK;
}