Skip to content

Commit eba7555

Browse files
committed
Experimental Proxmark3 support via PM3 CLI
1 parent c849e8c commit eba7555

File tree

10 files changed

+2459
-6
lines changed

10 files changed

+2459
-6
lines changed

internal/api/websocket.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ type WSClient struct {
3636
send chan []byte
3737
hub *WSHub
3838
mu sync.Mutex
39-
subscribed map[string]bool // Track subscribed readers for auto-read
39+
closed bool // True when client is disconnected
40+
subscribed map[string]bool // Track subscribed readers for auto-read
4041
pollTickers map[string]*time.Ticker
4142
lastUIDs map[string]string // Track last seen UID per reader
4243
}
@@ -75,7 +76,10 @@ func (h *WSHub) Run() {
7576
h.mu.Lock()
7677
if _, ok := h.clients[client]; ok {
7778
delete(h.clients, client)
79+
client.mu.Lock()
80+
client.closed = true
7881
close(client.send)
82+
client.mu.Unlock()
7983
}
8084
h.mu.Unlock()
8185
case message := <-h.broadcast:
@@ -276,24 +280,50 @@ func (c *WSClient) handleMessage(msg WSMessage) {
276280
}
277281

278282
func (c *WSClient) sendResponse(id string, msgType string, payload interface{}) {
283+
c.mu.Lock()
284+
if c.closed {
285+
c.mu.Unlock()
286+
return
287+
}
288+
c.mu.Unlock()
289+
279290
payloadBytes, _ := json.Marshal(payload)
280291
response := WSMessage{
281292
Type: msgType,
282293
ID: id,
283294
Payload: payloadBytes,
284295
}
285296
responseBytes, _ := json.Marshal(response)
286-
c.send <- responseBytes
297+
298+
// Use non-blocking send to avoid panic if channel closes between check and send
299+
select {
300+
case c.send <- responseBytes:
301+
default:
302+
// Channel full or closed, drop the message
303+
}
287304
}
288305

289306
func (c *WSClient) sendError(id string, errMsg string) {
307+
c.mu.Lock()
308+
if c.closed {
309+
c.mu.Unlock()
310+
return
311+
}
312+
c.mu.Unlock()
313+
290314
response := WSMessage{
291315
Type: "error",
292316
ID: id,
293317
Error: errMsg,
294318
}
295319
responseBytes, _ := json.Marshal(response)
296-
c.send <- responseBytes
320+
321+
// Use non-blocking send to avoid panic if channel closes between check and send
322+
select {
323+
case c.send <- responseBytes:
324+
default:
325+
// Channel full or closed, drop the message
326+
}
297327
}
298328

299329
func (c *WSClient) handleListReaders(id string) {

internal/config/config.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"os"
55
"strconv"
6+
"time"
67
)
78

89
const (
@@ -12,8 +13,18 @@ const (
1213

1314
// Config holds the application configuration.
1415
type Config struct {
15-
Host string
16-
Port int
16+
Host string
17+
Port int
18+
Proxmark3 Proxmark3Config
19+
}
20+
21+
// Proxmark3Config holds Proxmark3-specific configuration.
22+
type Proxmark3Config struct {
23+
Enabled bool // Enable Proxmark3 support (NFC_AGENT_PROXMARK3=1)
24+
Path string // Custom path to pm3 binary (NFC_AGENT_PM3_PATH)
25+
Port string // Specific serial port (NFC_AGENT_PM3_PORT)
26+
PersistentMode bool // Use persistent subprocess (NFC_AGENT_PM3_PERSISTENT, default: true)
27+
IdleTimeout time.Duration // Idle timeout before killing subprocess (NFC_AGENT_PM3_IDLE_TIMEOUT, default: 60s)
1728
}
1829

1930
// Load reads configuration from environment variables with sensible defaults.
@@ -35,9 +46,34 @@ func Load() *Config {
3546
cfg.Host = host
3647
}
3748

49+
// Proxmark3 configuration
50+
cfg.Proxmark3 = Proxmark3Config{
51+
Enabled: os.Getenv("NFC_AGENT_PROXMARK3") == "1",
52+
Path: os.Getenv("NFC_AGENT_PM3_PATH"),
53+
Port: os.Getenv("NFC_AGENT_PM3_PORT"),
54+
PersistentMode: os.Getenv("NFC_AGENT_PM3_PERSISTENT") != "0", // Default ON
55+
IdleTimeout: parseIdleTimeout(os.Getenv("NFC_AGENT_PM3_IDLE_TIMEOUT")),
56+
}
57+
3858
return cfg
3959
}
4060

61+
// parseIdleTimeout parses the idle timeout from a string.
62+
// Returns 60s by default, -1 for "never" or "-1".
63+
func parseIdleTimeout(s string) time.Duration {
64+
if s == "" {
65+
return 60 * time.Second
66+
}
67+
if s == "-1" || s == "never" {
68+
return -1 // Never timeout
69+
}
70+
d, err := time.ParseDuration(s)
71+
if err != nil {
72+
return 60 * time.Second
73+
}
74+
return d
75+
}
76+
4177
// Address returns the formatted host:port address string.
4278
func (c *Config) Address() string {
4379
return c.Host + ":" + strconv.Itoa(c.Port)

0 commit comments

Comments
 (0)