@@ -6,6 +6,32 @@ import { validateUrl } from "../utils/safe-exec";
66const POLL_INTERVAL_MS = parseInt ( process . env . MESHTASTIC_POLL_INTERVAL_MS || "3000" , 10 ) ;
77const 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
1036if ( 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
1743export 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 ;
0 commit comments