mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-25 14:44:01 +00:00
fix: filter garbage channel names from /api/channels, fixes #201
Channels with garbage-decrypted names (pre-#197 data still in DB) are now filtered at the API level using the same non-printable character heuristic from #197. Applied in both Node.js server.js and Go server (store.go, db.go). No data is deleted — only filtered from API responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1218,6 +1218,12 @@ func (db *DB) GetChannels() ([]map[string]interface{}, error) {
|
||||
if dtype != "CHAN" {
|
||||
continue
|
||||
}
|
||||
// Filter out garbage-decrypted channel names/messages (pre-#197 data still in DB)
|
||||
chanStr, _ := decoded["channel"].(string)
|
||||
textStr, _ := decoded["text"].(string)
|
||||
if hasGarbageChars(chanStr) || hasGarbageChars(textStr) {
|
||||
continue
|
||||
}
|
||||
channelName, _ := decoded["channel"].(string)
|
||||
if channelName == "" {
|
||||
channelName = "unknown"
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// payloadTypeNames maps payload_type int → human-readable name (firmware-standard).
|
||||
@@ -1538,6 +1539,25 @@ func filterTxSlice(s []*StoreTx, fn func(*StoreTx) bool) []*StoreTx {
|
||||
return result
|
||||
}
|
||||
|
||||
// countNonPrintable counts characters that are non-printable (< 0x20 except \n, \t)
|
||||
// or invalid UTF-8 replacement characters. Mirrors the heuristic from #197.
|
||||
func countNonPrintable(s string) int {
|
||||
count := 0
|
||||
for _, r := range s {
|
||||
if r < 0x20 && r != '\n' && r != '\t' {
|
||||
count++
|
||||
} else if r == utf8.RuneError {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// hasGarbageChars returns true if the string contains garbage (non-printable) data.
|
||||
func hasGarbageChars(s string) bool {
|
||||
return s != "" && (!utf8.ValidString(s) || countNonPrintable(s) > 2)
|
||||
}
|
||||
|
||||
// GetChannels returns channel list from in-memory packets (payload_type 5, decoded type CHAN).
|
||||
func (s *PacketStore) GetChannels(region string) []map[string]interface{} {
|
||||
s.mu.RLock()
|
||||
@@ -1588,6 +1608,10 @@ func (s *PacketStore) GetChannels(region string) []map[string]interface{} {
|
||||
if decoded.Type != "CHAN" {
|
||||
continue
|
||||
}
|
||||
// Filter out garbage-decrypted channel names/messages (pre-#197 data still in DB)
|
||||
if hasGarbageChars(decoded.Channel) || hasGarbageChars(decoded.Text) {
|
||||
continue
|
||||
}
|
||||
|
||||
channelName := decoded.Channel
|
||||
if channelName == "" {
|
||||
|
||||
+1
-1
@@ -360,7 +360,7 @@ function validateAdvert(advert) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
module.exports = { decodePacket, validateAdvert, ROUTE_TYPES, PAYLOAD_TYPES, VALID_ROLES };
|
||||
module.exports = { decodePacket, validateAdvert, hasNonPrintableChars, ROUTE_TYPES, PAYLOAD_TYPES, VALID_ROLES };
|
||||
|
||||
// --- Tests ---
|
||||
if (require.main === module) {
|
||||
|
||||
@@ -121,6 +121,28 @@ function seedTestData() {
|
||||
try { pktStore.insert(chanPkt2); } catch {}
|
||||
try { db.insertTransmission(chanPkt2); } catch {}
|
||||
|
||||
// Seed a CHAN packet with garbage-decrypted name (pre-#197 data) — should be filtered out
|
||||
const garbageChanPkt = {
|
||||
raw_hex: 'GARBAGE0CHANNEL1',
|
||||
timestamp: now, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 2, rssi: -95,
|
||||
hash: 'test-hash-garbage-chan', route_type: 0, payload_type: 5, payload_version: 1,
|
||||
path_json: JSON.stringify([]),
|
||||
decoded_json: JSON.stringify({ type: 'CHAN', channel: 'garb\x01\x02\x03age', text: 'SomeUser: hello\x04\x05\x06', sender: 'SomeUser' }),
|
||||
};
|
||||
try { pktStore.insert(garbageChanPkt); } catch {}
|
||||
try { db.insertTransmission(garbageChanPkt); } catch {}
|
||||
|
||||
// Seed a CHAN packet with clean name but garbage text — should also be filtered out
|
||||
const garbageTextPkt = {
|
||||
raw_hex: 'GARBAGE0TEXT0001',
|
||||
timestamp: now, observer_id: 'test-obs-1', observer_name: 'TestObs', snr: 2, rssi: -95,
|
||||
hash: 'test-hash-garbage-text', route_type: 0, payload_type: 5, payload_version: 1,
|
||||
path_json: JSON.stringify([]),
|
||||
decoded_json: JSON.stringify({ type: 'CHAN', channel: 'cleanChan', text: '\x00\x01\x02\x03garbage binary', sender: 'User' }),
|
||||
};
|
||||
try { pktStore.insert(garbageTextPkt); } catch {}
|
||||
try { db.insertTransmission(garbageTextPkt); } catch {}
|
||||
|
||||
// Packet with sender_key/recipient_key for peer interaction coverage in db.getNodeAnalytics
|
||||
const peerPkt = {
|
||||
raw_hex: 'DEADBEEF00112233',
|
||||
@@ -436,6 +458,17 @@ seedTestData();
|
||||
assert(typeof r.body === 'object', 'should return channels');
|
||||
});
|
||||
|
||||
await t('GET /api/channels filters garbage channel names', async () => {
|
||||
cache.clear();
|
||||
const r = await request(app).get('/api/channels').expect(200);
|
||||
const names = r.body.channels.map(c => c.name);
|
||||
assert(!names.some(n => n.includes('\x01') || n.includes('\x02') || n.includes('\x03')),
|
||||
'garbage channel names should be filtered out');
|
||||
assert(!names.includes('cleanChan'),
|
||||
'channels with garbage text should be filtered out');
|
||||
assert(names.includes('ch01'), 'valid channel ch01 should still be present');
|
||||
});
|
||||
|
||||
await t('GET /api/channels with region', async () => {
|
||||
await request(app).get('/api/channels?region=SFO').expect(200);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user