1+ import * as vscode from 'vscode' ;
2+ import * as https from 'https' ;
3+ import * as http from 'http' ;
4+ import { URL } from 'url' ;
5+ import { SSHHost , SSHGroup , RemoteHostsConfig , RemoteResponse } from './types' ;
6+
7+ export class RemoteHostsService {
8+ private cache : Map < string , { response : RemoteResponse , timestamp : number } > = new Map ( ) ;
9+ private readonly CACHE_DURATION = 5 * 60 * 1000 ; // 5 minutes cache
10+
11+ constructor ( ) { }
12+
13+ private processUrl ( url : string ) : string {
14+ return url . replace ( / \[ t i m e s t a m p \] / g, Date . now ( ) . toString ( ) ) ;
15+ }
16+
17+ async fetchRemoteData ( config : RemoteHostsConfig ) : Promise < RemoteResponse > {
18+ const cacheKey = this . getCacheKey ( config ) ;
19+ const cached = this . cache . get ( cacheKey ) ;
20+
21+ if ( cached && ( Date . now ( ) - cached . timestamp ) < this . CACHE_DURATION ) {
22+ return cached . response ;
23+ }
24+
25+ try {
26+ const response = await this . downloadRemoteData ( config ) ;
27+ this . cache . set ( cacheKey , { response, timestamp : Date . now ( ) } ) ;
28+ return response ;
29+ } catch ( error ) {
30+ console . error ( 'Failed to fetch remote data:' , error ) ;
31+
32+ if ( cached ) {
33+ vscode . window . showWarningMessage ( `Failed to fetch remote data, using cached data. Error: ${ error } ` ) ;
34+ return cached . response ;
35+ }
36+
37+ throw error ;
38+ }
39+ }
40+
41+ private async downloadRemoteData ( config : RemoteHostsConfig ) : Promise < RemoteResponse > {
42+ return new Promise ( ( resolve , reject ) => {
43+ const processedAddress = this . processUrl ( config . address ) ;
44+ const url = new URL ( processedAddress ) ;
45+ const isHttps = url . protocol === 'https:' ;
46+ const client = isHttps ? https : http ;
47+
48+ const headers : { [ key : string ] : string } = { } ;
49+
50+ if ( config . basicAuth ) {
51+ const auth = Buffer . from ( `${ config . basicAuth . username } :${ config . basicAuth . password } ` ) . toString ( 'base64' ) ;
52+ headers [ 'Authorization' ] = `Basic ${ auth } ` ;
53+ }
54+
55+ const options : http . RequestOptions = {
56+ hostname : url . hostname ,
57+ port : url . port || ( isHttps ? 443 : 80 ) ,
58+ path : url . pathname + url . search ,
59+ method : 'GET' ,
60+ headers
61+ } ;
62+
63+ const req = client . request ( options , ( res ) => {
64+ let data = '' ;
65+
66+ res . on ( 'data' , ( chunk ) => {
67+ data += chunk ;
68+ } ) ;
69+
70+ res . on ( 'end' , ( ) => {
71+ try {
72+ if ( res . statusCode && res . statusCode >= 200 && res . statusCode < 300 ) {
73+ const response = this . parseRemoteData ( data ) ;
74+ resolve ( response ) ;
75+ } else {
76+ reject ( new Error ( `HTTP ${ res . statusCode } : ${ res . statusMessage } ` ) ) ;
77+ }
78+ } catch ( error ) {
79+ reject ( new Error ( `Failed to parse remote data: ${ error } ` ) ) ;
80+ }
81+ } ) ;
82+ } ) ;
83+
84+ req . on ( 'error' , ( error ) => {
85+ reject ( error ) ;
86+ } ) ;
87+
88+ req . setTimeout ( 10000 , ( ) => {
89+ req . destroy ( ) ;
90+ reject ( new Error ( 'Request timeout' ) ) ;
91+ } ) ;
92+
93+ req . end ( ) ;
94+ } ) ;
95+ }
96+
97+ private parseRemoteData ( data : string ) : RemoteResponse {
98+ try {
99+ const jsonData = JSON . parse ( data ) ;
100+
101+ if ( Array . isArray ( jsonData ) ) {
102+ return { hosts : jsonData } ;
103+ } else if ( jsonData . hosts || jsonData . groups ) {
104+ return {
105+ hosts : jsonData . hosts || [ ] ,
106+ groups : jsonData . groups || [ ]
107+ } ;
108+ } else if ( jsonData . hosts && Array . isArray ( jsonData . hosts ) ) {
109+ return { hosts : jsonData . hosts } ;
110+ } else {
111+ throw new Error ( 'Invalid JSON structure' ) ;
112+ }
113+ } catch ( jsonError ) {
114+ const hosts = this . parseTextFormat ( data ) ;
115+ return { hosts } ;
116+ }
117+ }
118+
119+ private parseTextFormat ( data : string ) : SSHHost [ ] {
120+ const hosts : SSHHost [ ] = [ ] ;
121+ const lines = data . split ( '\n' ) . filter ( line => line . trim ( ) . length > 0 ) ;
122+
123+ for ( const line of lines ) {
124+ const trimmed = line . trim ( ) ;
125+
126+ if ( trimmed . startsWith ( '#' ) || trimmed . startsWith ( '//' ) ) {
127+ continue ;
128+ }
129+
130+ const parts = trimmed . split ( ':' ) ;
131+
132+ if ( parts . length >= 1 ) {
133+ const host : SSHHost = {
134+ hostName : parts [ 0 ] . trim ( ) ,
135+ name : parts [ 1 ] ?. trim ( ) || parts [ 0 ] . trim ( )
136+ } ;
137+
138+ if ( parts [ 2 ] ?. trim ( ) ) {
139+ host . user = parts [ 2 ] . trim ( ) ;
140+ }
141+
142+ if ( parts [ 3 ] ?. trim ( ) ) {
143+ const port = parseInt ( parts [ 3 ] . trim ( ) , 10 ) ;
144+ if ( ! isNaN ( port ) ) {
145+ host . port = port ;
146+ }
147+ }
148+
149+ hosts . push ( host ) ;
150+ }
151+ }
152+
153+ return hosts ;
154+ }
155+
156+ private getCacheKey ( config : RemoteHostsConfig ) : string {
157+ return `${ config . address } :${ config . basicAuth ?. username || 'noauth' } ` ;
158+ }
159+
160+ clearCache ( ) : void {
161+ this . cache . clear ( ) ;
162+ }
163+
164+ getCacheInfo ( ) : Array < { url : string , hostsCount : number , groupsCount : number , age : number } > {
165+ const info : Array < { url : string , hostsCount : number , groupsCount : number , age : number } > = [ ] ;
166+ const now = Date . now ( ) ;
167+
168+ for ( const [ key , value ] of this . cache . entries ( ) ) {
169+ const url = key . split ( ':' ) [ 0 ] ;
170+ info . push ( {
171+ url,
172+ hostsCount : value . response . hosts ?. length || 0 ,
173+ groupsCount : value . response . groups ?. length || 0 ,
174+ age : Math . floor ( ( now - value . timestamp ) / 1000 )
175+ } ) ;
176+ }
177+
178+ return info ;
179+ }
180+ }
0 commit comments