Skip to content

Commit 6b083b3

Browse files
committed
refactor: Separate raw and state-based output modules with security fixes
- Fix port collision: Change AVR default port from 30003 to 30001 - Introduce OutputModuleBase, RawOutputModule, and StateOutputModule traits - Separate broadcast pipelines for raw packets vs aircraft state - BREAKING: WebSocket now streams SBS-1 CSV format instead of BEAST binary - Replace unsafe unwrap() calls with expect() and if-let patterns in tracker - Update documentation with WebSocket format change and security model - Document WebSocket as local-only with no authentication/encryption This refactoring enables clean separation between pass-through protocols (BEAST, Raw, AVR) and stateful protocols (SBS-1, WebSocket) that require accumulated aircraft data.
1 parent 29bd2f1 commit 6b083b3

File tree

11 files changed

+943
-372
lines changed

11 files changed

+943
-372
lines changed

CLAUDE.md

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,14 @@ AirJedi supports multiple output formats for compatibility with various ADS-B to
160160
- **Protocol**: `*{hexdata};\n` format
161161
- **Compatibility**: dump1090 port 30002, simple monitoring tools
162162

163-
### AVR Format (Port 30003)
163+
### AVR Format (Port 30001)
164164
- **Default**: Disabled by default
165165
- **Description**: Text format with timestamps and signal levels
166166
- **Usage**: `--avr`, `--avr-port <PORT>`
167167
- **Protocol**: `@{timestamp}\n*{hexdata};\n` format
168168
- **Compatibility**: dump1090 AVR format, debugging tools
169169

170-
### SBS-1/BaseStation Format (Port 30004)
170+
### SBS-1/BaseStation Format (Port 30003)
171171
- **Default**: Disabled by default
172172
- **Description**: CSV format compatible with BaseStation and SBS-1 receivers
173173
- **Usage**: `--sbs1`, `--sbs1-port <PORT>`
@@ -179,12 +179,13 @@ AirJedi supports multiple output formats for compatibility with various ADS-B to
179179

180180
### WebSocket Format (Port 8080)
181181
- **Default**: Disabled by default
182-
- **Description**: Real-time WebSocket streaming with BEAST format messages
182+
- **Description**: Real-time WebSocket streaming with SBS-1 CSV format messages
183183
- **Usage**: `--websocket`, `--websocket-port <PORT>`
184-
- **Protocol**: Binary WebSocket messages containing BEAST-encoded ADS-B data
184+
- **Protocol**: Text WebSocket messages containing SBS-1/BaseStation CSV data
185185
- **Compatibility**: Web browsers, JavaScript applications, real-time web dashboards
186-
- **Message Format**: Each WebSocket message contains a complete BEAST-format packet
186+
- **Message Format**: Each WebSocket message contains SBS-1 CSV formatted aircraft data (MSG,1/2/3/4)
187187
- **Use Cases**: Real-time web applications, live flight tracking interfaces, custom dashboards
188+
- **Note**: WebSocket broadcasts state-based updates (identification, position, velocity) rather than raw packets
188189

189190
## Example Usage
190191

@@ -196,7 +197,7 @@ cargo run
196197
cargo run -- --avr --sbs1 --websocket
197198

198199
# Custom ports to avoid conflicts
199-
cargo run -- --beast-port 40005 --raw-port 40002 --sbs1-port 40004 --websocket-port 9090
200+
cargo run -- --beast-port 40005 --raw-port 40002 --sbs1-port 40003 --websocket-port 9090
200201

201202
# Enable only WebSocket output for web applications
202203
cargo run -- --no-beast --no-raw --websocket
@@ -213,23 +214,41 @@ For web applications connecting to the WebSocket output:
213214
// Connect to AirJedi WebSocket server
214215
const ws = new WebSocket('ws://localhost:8080');
215216

216-
// Handle binary BEAST messages
217+
// Handle SBS-1 CSV text messages
217218
ws.onmessage = function(event) {
218-
// event.data is an ArrayBuffer containing BEAST format data
219-
const data = new Uint8Array(event.data);
220-
221-
// BEAST format: [0x1A, type, timestamp(6), signal, payload...]
222-
if (data[0] === 0x1A) { // BEAST escape character
223-
const messageType = data[1]; // 0x31=short, 0x32=long
224-
const timestamp = data.slice(2, 8); // 6 bytes
225-
const signal = data[8]; // Signal strength
226-
const payload = data.slice(9); // ADS-B message
227-
228-
console.log('ADS-B Message:', {
229-
type: messageType === 0x31 ? 'short' : 'long',
230-
signal: signal,
231-
data: Array.from(payload).map(b => b.toString(16).padStart(2, '0')).join('')
232-
});
219+
// event.data is a string containing SBS-1 CSV format data
220+
const csvData = event.data;
221+
const fields = csvData.split(',');
222+
223+
// SBS-1 format: MSG,type,session,aircraft,icao,flight,date_gen,time_gen,date_log,time_log,...
224+
if (fields[0] === 'MSG') {
225+
const messageType = parseInt(fields[1]);
226+
const icao = fields[4];
227+
228+
switch (messageType) {
229+
case 1: // Identification (callsign)
230+
console.log('Aircraft Identification:', {
231+
icao: icao,
232+
callsign: fields[10]
233+
});
234+
break;
235+
case 3: // Airborne Position
236+
console.log('Aircraft Position:', {
237+
icao: icao,
238+
altitude: fields[11],
239+
latitude: fields[14],
240+
longitude: fields[15]
241+
});
242+
break;
243+
case 4: // Airborne Velocity
244+
console.log('Aircraft Velocity:', {
245+
icao: icao,
246+
groundSpeed: fields[12],
247+
track: fields[13],
248+
verticalRate: fields[16]
249+
});
250+
break;
251+
}
233252
}
234253
};
235254

@@ -240,4 +259,38 @@ ws.onopen = function() {
240259
ws.onerror = function(error) {
241260
console.error('WebSocket error:', error);
242261
};
243-
```
262+
```
263+
264+
## WebSocket Security Model
265+
266+
**IMPORTANT**: The WebSocket server is designed for **local-only use** and has minimal security controls:
267+
268+
### Security Characteristics
269+
270+
- **No Authentication**: Any client that can reach the port can connect and receive data
271+
- **No Authorization**: All connected clients receive all aircraft data
272+
- **No Origin Validation**: No CORS checks - any website can connect via JavaScript
273+
- **No TLS/Encryption**: Plain TCP WebSocket (ws://) without encryption (wss:// not supported)
274+
- **No Rate Limiting**: Clients can connect and consume data without throttling
275+
- **Public Data**: ADS-B data is inherently public information broadcast by aircraft
276+
277+
### Recommended Deployment
278+
279+
1. **Bind to localhost only** (default: 127.0.0.1:8080) - prevents external access
280+
2. **Use firewall rules** to restrict access if binding to non-localhost interfaces
281+
3. **Deploy behind reverse proxy** if internet access is needed (add authentication/TLS there)
282+
4. **Monitor connections** via client_count metrics to detect unexpected clients
283+
284+
### Acceptable Use Cases
285+
286+
- ✅ Local development and testing
287+
- ✅ Single-user desktop applications
288+
- ✅ Trusted local network deployments
289+
- ✅ Behind authenticated reverse proxy (nginx, Apache with auth)
290+
291+
### Unsuitable Use Cases
292+
293+
- ❌ Direct internet exposure without additional security layers
294+
- ❌ Multi-tenant environments without isolation
295+
- ❌ Scenarios requiring data access auditing
296+
- ❌ Compliance-regulated environments without additional controls

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ authors = ["ccustine"]
1313

1414
[[bin]]
1515
name = "airjedi"
16-
path = "src/bin/listen_adsb.rs"
16+
path = "src/bin/airjedi.rs"
1717

1818
[features]
1919
default = ["soapy"]

src/avr_output.rs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! output with timestamps and signal levels for debugging and integration.
66
77
use crate::decoder::DecoderMetaData;
8+
use crate::output_module::{OutputModuleBase, RawOutputModule};
89
use anyhow::Result;
910
use std::time::UNIX_EPOCH;
1011
use tokio::io::AsyncWriteExt;
@@ -191,24 +192,20 @@ impl AvrOutput {
191192
}
192193
}
193194

194-
#[async_trait::async_trait]
195-
impl crate::output_module::OutputModule for AvrOutput {
195+
// Implement the base trait for common functionality
196+
impl crate::output_module::OutputModuleBase for AvrOutput {
196197
fn name(&self) -> &str {
197198
&self.name
198199
}
199200

200201
fn description(&self) -> &str {
201-
"AVR text format with timestamps for dump1090 compatibility (port 30003)"
202+
"AVR text format with timestamps for dump1090 compatibility (port 30001)"
202203
}
203204

204205
fn port(&self) -> u16 {
205206
self.port
206207
}
207208

208-
fn broadcast_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
209-
self.broadcaster.broadcast_packet(data, metadata)
210-
}
211-
212209
fn client_count(&self) -> usize {
213210
self.broadcaster.client_count()
214211
}
@@ -223,6 +220,46 @@ impl crate::output_module::OutputModule for AvrOutput {
223220
}
224221
}
225222

223+
// Implement the raw output trait for broadcasting raw packets
224+
#[async_trait::async_trait]
225+
impl crate::output_module::RawOutputModule for AvrOutput {
226+
fn broadcast_raw_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
227+
self.broadcaster.broadcast_packet(data, metadata)
228+
}
229+
}
230+
231+
// Keep legacy trait implementation for backward compatibility during migration
232+
#[async_trait::async_trait]
233+
impl crate::output_module::OutputModule for AvrOutput {
234+
fn name(&self) -> &str {
235+
OutputModuleBase::name(self)
236+
}
237+
238+
fn description(&self) -> &str {
239+
OutputModuleBase::description(self)
240+
}
241+
242+
fn port(&self) -> u16 {
243+
OutputModuleBase::port(self)
244+
}
245+
246+
fn broadcast_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
247+
self.broadcast_raw_packet(data, metadata)
248+
}
249+
250+
fn client_count(&self) -> usize {
251+
OutputModuleBase::client_count(self)
252+
}
253+
254+
fn is_running(&self) -> bool {
255+
OutputModuleBase::is_running(self)
256+
}
257+
258+
fn stop(&mut self) -> Result<()> {
259+
OutputModuleBase::stop(self)
260+
}
261+
}
262+
226263
// Builder implementation removed - using direct instantiation in main
227264

228265
#[cfg(test)]

src/beast_output.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! existing ADS-B tools like tar1090, FlightAware feeders, and other aggregators.
66
77
use crate::decoder::DecoderMetaData;
8+
use crate::output_module::{OutputModuleBase, RawOutputModule};
89
use anyhow::Result;
910
use std::time::UNIX_EPOCH;
1011
use tokio::io::AsyncWriteExt;
@@ -229,8 +230,8 @@ impl BeastOutput {
229230
}
230231
}
231232

232-
#[async_trait::async_trait]
233-
impl crate::output_module::OutputModule for BeastOutput {
233+
// Implement the base trait for common functionality
234+
impl crate::output_module::OutputModuleBase for BeastOutput {
234235
fn name(&self) -> &str {
235236
&self.name
236237
}
@@ -243,10 +244,6 @@ impl crate::output_module::OutputModule for BeastOutput {
243244
self.port
244245
}
245246

246-
fn broadcast_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
247-
self.broadcaster.broadcast_packet(data, metadata)
248-
}
249-
250247
fn client_count(&self) -> usize {
251248
self.broadcaster.client_count()
252249
}
@@ -261,6 +258,46 @@ impl crate::output_module::OutputModule for BeastOutput {
261258
}
262259
}
263260

261+
// Implement the raw output trait for broadcasting raw packets
262+
#[async_trait::async_trait]
263+
impl crate::output_module::RawOutputModule for BeastOutput {
264+
fn broadcast_raw_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
265+
self.broadcaster.broadcast_packet(data, metadata)
266+
}
267+
}
268+
269+
// Keep legacy trait implementation for backward compatibility during migration
270+
#[async_trait::async_trait]
271+
impl crate::output_module::OutputModule for BeastOutput {
272+
fn name(&self) -> &str {
273+
OutputModuleBase::name(self)
274+
}
275+
276+
fn description(&self) -> &str {
277+
OutputModuleBase::description(self)
278+
}
279+
280+
fn port(&self) -> u16 {
281+
OutputModuleBase::port(self)
282+
}
283+
284+
fn broadcast_packet(&self, data: &[u8], metadata: &DecoderMetaData) -> Result<()> {
285+
self.broadcast_raw_packet(data, metadata)
286+
}
287+
288+
fn client_count(&self) -> usize {
289+
OutputModuleBase::client_count(self)
290+
}
291+
292+
fn is_running(&self) -> bool {
293+
OutputModuleBase::is_running(self)
294+
}
295+
296+
fn stop(&mut self) -> Result<()> {
297+
OutputModuleBase::stop(self)
298+
}
299+
}
300+
264301
// Builder implementation removed - using direct instantiation in main
265302

266303
#[cfg(test)]

0 commit comments

Comments
 (0)