Skip to content

Commit 02865a4

Browse files
committed
Add flags for meshtasticd connection and self-signed cert support
- Revert default port to 4403 (original behavior) - Add --port/-P flag to specify custom HTTP port - Add --tls/-T flag to use HTTPS - Add --insecure/-k flag to accept self-signed SSL certificates - Update README with examples for meshtasticd connections - Note that meshtasticd webserver must be enabled (port 8080)
1 parent 9735709 commit 02865a4

4 files changed

Lines changed: 123 additions & 25 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,36 @@ Options:
9494
--skip-nodes Skip downloading node database (faster connect)
9595
--meshview, -m MeshView URL for packet/node links
9696
--fahrenheit, -F Display temperatures in Fahrenheit
97+
--port, -P HTTP port number (default: 4403 if no port in address)
98+
--tls, -T Use HTTPS instead of HTTP
99+
--insecure, -k Accept self-signed SSL certificates
97100
--help, -h Show help
98101
```
99102

103+
### Examples
104+
105+
Connect to a Meshtastic device (default port 4403):
106+
```sh
107+
meshtastic-cli 192.168.0.123
108+
```
109+
110+
Connect to meshtasticd HTTP API (port 8080 with HTTPS):
111+
```sh
112+
meshtastic-cli 127.0.0.1 --port 8080 --tls --insecure
113+
```
114+
115+
> **Note:** meshtasticd servers need to have the webserver enabled. The webserver typically runs on port 8080 with HTTPS. Check your meshtasticd configuration to ensure the webserver is enabled.
116+
117+
Connect to a device on a custom port:
118+
```sh
119+
meshtastic-cli 192.168.1.100 --port 8080
120+
```
121+
122+
Connect with HTTPS and accept self-signed certificate:
123+
```sh
124+
meshtastic-cli example.com --tls --insecure
125+
```
126+
100127
## Message Status Indicators
101128

102129
In Chat and DM views, messages show delivery status:

src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ let meshViewUrl: string | undefined;
109109
let useFahrenheit = false;
110110
let enableLogging = true;
111111
let packetLimit = 1000;
112+
let httpPort: number | undefined;
113+
let useTls = false;
114+
let insecure = false;
112115

113116
for (let i = 0; i < args.length; i++) {
114117
const arg = args[i];
@@ -148,6 +151,22 @@ for (let i = 0; i < args.length; i++) {
148151
process.exit(1);
149152
}
150153
packetLimit = limit;
154+
} else if (arg === "--port" || arg === "-P") {
155+
const portArg = args[++i];
156+
if (!portArg) {
157+
console.error("--port requires a port number");
158+
process.exit(1);
159+
}
160+
const port = parseInt(portArg, 10);
161+
if (isNaN(port) || port < 1 || port > 65535) {
162+
console.error("Port must be between 1 and 65535");
163+
process.exit(1);
164+
}
165+
httpPort = port;
166+
} else if (arg === "--tls" || arg === "-T") {
167+
useTls = true;
168+
} else if (arg === "--insecure" || arg === "-k") {
169+
insecure = true;
151170
} else if (arg === "--help" || arg === "-h") {
152171
console.log(`
153172
Meshtastic CLI Viewer
@@ -165,6 +184,9 @@ Options:
165184
--meshview, -m MeshView URL for packet/node links (default: from settings or disabled)
166185
--fahrenheit, -F Display temperatures in Fahrenheit instead of Celsius
167186
--packet-limit, -p Maximum packets to store in database (default: 1000)
187+
--port, -P HTTP port number (default: 4403 if no port in address)
188+
--tls, -T Use HTTPS instead of HTTP
189+
--insecure, -k Accept self-signed SSL certificates
168190
--enable-logging, -L Enable verbose logging to ~/.config/meshtastic-cli/log
169191
--help, -h Show this help message
170192
`);
@@ -239,6 +261,9 @@ const { waitUntilExit } = render(
239261
skipNodes,
240262
meshViewUrl: resolvedMeshViewUrl,
241263
useFahrenheit,
264+
httpPort,
265+
useTls,
266+
insecure,
242267
})
243268
);
244269

src/transport/http.ts

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,32 @@ import { validateUrl } from "../utils/safe-exec";
66
const POLL_INTERVAL_MS = parseInt(process.env.MESHTASTIC_POLL_INTERVAL_MS || "3000", 10);
77
const TIMEOUT_MS = parseInt(process.env.MESHTASTIC_TIMEOUT_MS || "5000", 10);
88

9+
// Helper to check if URL is localhost (for self-signed cert handling)
10+
function isLocalhost(url: string): boolean {
11+
try {
12+
const parsed = new URL(url);
13+
const hostname = parsed.hostname.toLowerCase();
14+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.startsWith("127.") || hostname === "[::1]";
15+
} catch {
16+
return false;
17+
}
18+
}
19+
20+
// Helper to create fetch options with TLS configuration for self-signed certs
21+
function getFetchOptions(url: string, insecure: boolean, additionalOptions: RequestInit = {}): RequestInit {
22+
const options: RequestInit = { ...additionalOptions };
23+
24+
// Accept self-signed certificates if insecure flag is set or if connecting to localhost
25+
if (url.startsWith("https://") && (insecure || isLocalhost(url))) {
26+
// Bun's fetch supports tls option to configure TLS
27+
(options as any).tls = {
28+
rejectUnauthorized: false,
29+
};
30+
}
31+
32+
return options;
33+
}
34+
935
// Validate timeout values
1036
if (isNaN(POLL_INTERVAL_MS) || POLL_INTERVAL_MS < 100 || POLL_INTERVAL_MS > 60000) {
1137
throw new Error(`Invalid POLL_INTERVAL_MS: ${process.env.MESHTASTIC_POLL_INTERVAL_MS}. Must be between 100 and 60000`);
@@ -16,6 +42,7 @@ if (isNaN(TIMEOUT_MS) || TIMEOUT_MS < 1000 || TIMEOUT_MS > 60000) {
1642

1743
export class HttpTransport implements Transport {
1844
private url: string;
45+
private insecure: boolean;
1946
private running = false;
2047
private outputs: DeviceOutput[] = [];
2148
private resolvers: Array<(value: IteratorResult<DeviceOutput>) => void> = [];
@@ -24,11 +51,12 @@ export class HttpTransport implements Transport {
2451
private consecutiveErrors = 0;
2552
private readonly MAX_CONSECUTIVE_ERRORS = 10;
2653

27-
constructor(url: string) {
54+
constructor(url: string, insecure = false) {
2855
this.url = url.replace(/\/$/, "");
56+
this.insecure = insecure;
2957
}
3058

31-
static async create(address: string, tls = false): Promise<HttpTransport> {
59+
static async create(address: string, tls = false, port?: number, insecure = false): Promise<HttpTransport> {
3260
// Validate address format
3361
try {
3462
// Basic validation - address should not contain protocol
@@ -40,7 +68,26 @@ export class HttpTransport implements Transport {
4068
throw error;
4169
}
4270

43-
const url = `${tls ? "https" : "http"}://${address}`;
71+
// Check if address already includes a port
72+
const hasPort = /:\d+$/.test(address) || /]:\d+$/.test(address);
73+
let addressWithPort = address;
74+
let useTlsFlag = tls;
75+
76+
if (!hasPort) {
77+
// If no port in address, use provided port or default to 4403
78+
const defaultPort = port || 4403;
79+
addressWithPort = `${address}:${defaultPort}`;
80+
}
81+
// If port is provided via flag but address also has a port, flag takes precedence
82+
else if (port) {
83+
// Extract hostname/IP from address
84+
const hostnameMatch = address.match(/^(.+):\d+$/);
85+
if (hostnameMatch) {
86+
addressWithPort = `${hostnameMatch[1]}:${port}`;
87+
}
88+
}
89+
90+
const url = `${useTlsFlag ? "https" : "http"}://${addressWithPort}`;
4491

4592
// Validate the constructed URL
4693
try {
@@ -50,20 +97,16 @@ export class HttpTransport implements Transport {
5097
throw error;
5198
}
5299

53-
Logger.info("HttpTransport", "Attempting connection", { address, tls, url });
54-
try {
55-
await fetch(`${url}/api/v1/fromradio`, {
56-
method: "GET",
57-
signal: AbortSignal.timeout(TIMEOUT_MS),
58-
});
59-
Logger.info("HttpTransport", "Connection successful", { url });
60-
const transport = new HttpTransport(url);
61-
transport.startPolling();
62-
return transport;
63-
} catch (error) {
64-
Logger.error("HttpTransport", "Connection failed", error as Error, { url });
65-
throw error;
66-
}
100+
Logger.info("HttpTransport", "Attempting connection", { address, tls: useTlsFlag, url });
101+
102+
// Skip strict connection test - start polling immediately
103+
// The polling loop will handle connection errors gracefully
104+
// This is more resilient for cases where the server accepts connections
105+
// but endpoints may hang or require specific conditions
106+
Logger.info("HttpTransport", "Starting transport (connection will be verified during polling)", { url, insecure });
107+
const transport = new HttpTransport(url, insecure);
108+
transport.startPolling();
109+
return transport;
67110
}
68111

69112
private startPolling() {
@@ -87,11 +130,11 @@ export class HttpTransport implements Transport {
87130
let batchCount = 0;
88131
while (gotPacket && this.running && batchCount < 50) {
89132
Logger.debug("HttpTransport", "Polling for packets", { url: `${this.url}/api/v1/fromradio` });
90-
const response = await fetch(`${this.url}/api/v1/fromradio?all=false`, {
133+
const response = await fetch(`${this.url}/api/v1/fromradio?all=false`, getFetchOptions(this.url, this.insecure, {
91134
method: "GET",
92135
headers: { Accept: "application/x-protobuf" },
93136
signal: AbortSignal.timeout(TIMEOUT_MS),
94-
});
137+
}));
95138

96139
if (!response.ok) {
97140
Logger.warn("HttpTransport", "HTTP error response", { status: response.status, statusText: response.statusText });
@@ -197,12 +240,12 @@ export class HttpTransport implements Transport {
197240
async send(data: Uint8Array): Promise<void> {
198241
Logger.info("HttpTransport", "Sending packet", { size: data.byteLength, url: `${this.url}/api/v1/toradio` });
199242
try {
200-
const response = await fetch(`${this.url}/api/v1/toradio`, {
243+
const response = await fetch(`${this.url}/api/v1/toradio`, getFetchOptions(this.url, this.insecure, {
201244
method: "PUT",
202245
headers: { "Content-Type": "application/x-protobuf" },
203246
body: Buffer.from(data),
204247
signal: AbortSignal.timeout(TIMEOUT_MS),
205-
});
248+
}));
206249
if (!response.ok) {
207250
Logger.error("HttpTransport", "Send failed", undefined, { status: response.status, statusText: response.statusText });
208251
throw new Error(`HTTP ${response.status}`);
@@ -229,11 +272,11 @@ export class HttpTransport implements Transport {
229272
// Try /json/nodes endpoint to find local node (node with hopsAway=0 or smallest num)
230273
Logger.debug("HttpTransport", "Fetching owner info", { url: `${this.url}/json/nodes` });
231274
try {
232-
const response = await fetch(`${this.url}/json/nodes`, {
275+
const response = await fetch(`${this.url}/json/nodes`, getFetchOptions(this.url, this.insecure, {
233276
method: "GET",
234277
headers: { Accept: "application/json" },
235278
signal: AbortSignal.timeout(TIMEOUT_MS),
236-
});
279+
}));
237280
if (!response.ok) {
238281
Logger.warn("HttpTransport", "Failed to fetch owner", { status: response.status });
239282
return null;

src/ui/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ interface AppProps {
9595
skipNodes?: boolean;
9696
meshViewUrl?: string;
9797
useFahrenheit?: boolean;
98+
httpPort?: number;
99+
useTls?: boolean;
100+
insecure?: boolean;
98101
}
99102

100-
export function App({ address, packetStore, nodeStore, skipConfig = false, skipNodes = false, meshViewUrl, useFahrenheit = false }: AppProps) {
103+
export function App({ address, packetStore, nodeStore, skipConfig = false, skipNodes = false, meshViewUrl, useFahrenheit = false, httpPort, useTls = false, insecure = false }: AppProps) {
101104
const { exit } = useApp();
102105
const { stdout } = useStdout();
103106
const [transport, setTransport] = useState<Transport | null>(null);
@@ -153,7 +156,7 @@ export function App({ address, packetStore, nodeStore, skipConfig = false, skipN
153156
Logger.info("App", "Initiating device connection", { address });
154157
(async () => {
155158
try {
156-
const t = await HttpTransport.create(address);
159+
const t = await HttpTransport.create(address, useTls, httpPort, insecure);
157160
if (!cancelled) {
158161
Logger.info("App", "Transport created successfully", { address });
159162
setTransport(t);

0 commit comments

Comments
 (0)