UNPKG

lsp-gateway

Version:

Local LSP gateway for development - HTTP JSON-RPC and MCP server for multiple languages

611 lines (540 loc) 21.6 kB
const fs = require('fs'); const path = require('path'); const https = require('https'); const crypto = require('crypto'); const { spawn } = require('child_process'); const { promisify } = require('util'); const PlatformInfo = require('./platform'); const chmod = promisify(fs.chmod); const copyFile = promisify(fs.copyFile); const access = promisify(fs.access); const unlink = promisify(fs.unlink); const mkdir = promisify(fs.mkdir); const stat = promisify(fs.stat); /** * Binary installer for LSP Gateway * Downloads and installs the appropriate binary for the current platform */ class Installer { constructor() { this.platform = new PlatformInfo(); this.packageJson = require('../package.json'); this.version = this.packageJson.version; this.tempFiles = []; this.installStartTime = Date.now(); this.skipChecksumVerification = process.env.SKIP_CHECKSUM_VERIFICATION === 'true'; // SHA-256 checksums for binary integrity verification // TODO: These checksums need to be updated for each release this.checksums = { 'v1.0.0': { 'linux-x64': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'win32-x64': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'darwin-x64': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'darwin-arm64': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' } // Additional versions can be added here as they are released }; } /** * Calculate SHA-256 checksum of a file * @param {string} filePath - Path to the file * @returns {Promise<string>} SHA-256 hash in hexadecimal */ async calculateFileChecksum(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('data', (data) => { hash.update(data); }); stream.on('end', () => { resolve(hash.digest('hex')); }); stream.on('error', (error) => { reject(new Error(`Failed to calculate checksum: ${error.message}`)); }); }); } /** * Get expected checksum for current version and platform * @returns {string|null} Expected SHA-256 checksum or null if not found */ getExpectedChecksum() { const versionString = this.version.startsWith('v') ? this.version : `v${this.version}`; const platformKey = this.platform.getCurrentPlatformString(); if (this.checksums[versionString] && this.checksums[versionString][platformKey]) { return this.checksums[versionString][platformKey]; } return null; } /** * Verify binary integrity using SHA-256 checksum * @param {string} binaryPath - Path to the binary file * @returns {Promise<boolean>} True if checksum matches, false otherwise */ async verifyBinaryChecksum(binaryPath) { if (this.skipChecksumVerification) { console.log('⚠️ WARNING: Checksum verification is disabled via SKIP_CHECKSUM_VERIFICATION environment variable'); console.log(' This is NOT recommended for production use!'); return true; } const expectedChecksum = this.getExpectedChecksum(); if (!expectedChecksum) { console.log(`⚠️ No checksum available for version ${this.version} on ${this.platform.getCurrentPlatformString()}`); console.log(' Proceeding without checksum verification'); console.log(' WARNING: This reduces security against man-in-the-middle attacks'); return true; } console.log('🔐 Verifying binary integrity...'); try { const actualChecksum = await this.calculateFileChecksum(binaryPath); if (actualChecksum === expectedChecksum) { console.log('✅ Binary checksum verification successful'); return true; } else { console.error('❌ Binary checksum verification FAILED!'); console.error(` Expected: ${expectedChecksum}`); console.error(` Actual: ${actualChecksum}`); console.error(' This indicates the binary may have been tampered with or corrupted'); return false; } } catch (error) { console.error(`❌ Checksum verification failed: ${error.message}`); return false; } } /** * Main installation method * @returns {Promise<void>} */ async install() { try { console.log('🔧 Installing LSP Gateway...'); // Check platform support if (!this.platform.isSupported()) { throw new Error(`Platform ${this.platform.getCurrentPlatformString()} is not supported`); } console.log(`📦 Detected platform: ${this.platform.getPlatformName()}`); // Create bin directory if it doesn't exist const binDir = path.join(__dirname, '..', 'bin'); await this.ensureDirectory(binDir); // Check if binary already exists and works const binaryPath = this.platform.getBinaryPath(); if (await this.fileExists(binaryPath)) { console.log('✅ Binary already exists, checking if it works...'); if (await this.testBinary(binaryPath)) { const duration = Date.now() - this.installStartTime; console.log(`✅ LSP Gateway is already installed and working! (${duration}ms)`); return; } console.log('⚠️ Existing binary is not working, reinstalling...'); await this.cleanup([binaryPath]); } // Try multiple installation methods in order of preference const installMethods = [ { method: () => this.installFromBuildOutput(binaryPath), name: 'build-output', requiresChecksum: false }, { method: () => this.installFromLocalBinary(binaryPath), name: 'local-binary', requiresChecksum: false }, { method: () => this.installFromDownload(binaryPath), name: 'download', requiresChecksum: true } ]; let installed = false; let lastError; let usedMethod = null; for (const { method, name, requiresChecksum } of installMethods) { try { await method(); installed = true; usedMethod = { name, requiresChecksum }; break; } catch (error) { lastError = error; console.log(`⚠️ Installation method failed: ${error.message}`); } } if (!installed) { throw lastError || new Error('All installation methods failed'); } // Make binary executable await this.makeExecutable(binaryPath); // Verify binary integrity with checksum (primarily for downloaded binaries) if (usedMethod && usedMethod.requiresChecksum) { console.log('🔐 Verifying downloaded binary integrity...'); const isValidChecksum = await this.verifyBinaryChecksum(binaryPath); if (!isValidChecksum) { await this.cleanup([binaryPath]); throw new Error('Binary installation failed - checksum verification failed. The binary may have been tampered with or corrupted during download.'); } } else if (usedMethod) { console.log(`ℹ️ Using ${usedMethod.name} binary - checksum verification not required for local sources`); } // Test the binary if (await this.testBinary(binaryPath)) { const duration = Date.now() - this.installStartTime; console.log(`✅ LSP Gateway installed successfully! (${duration}ms)`); console.log(''); await this.showQuickStart(); await this.showPostInstallInfo(); } else { throw new Error('Binary installation failed - binary is not working properly'); } } catch (error) { await this.handleInstallationError(error); } finally { await this.cleanup(this.tempFiles); } } /** * Install from build output directory * @param {string} binaryPath - Destination binary path * @returns {Promise<void>} */ async installFromBuildOutput(binaryPath) { const sourcePath = this.platform.getSourceBinaryPath(); if (await this.fileExists(sourcePath)) { console.log('🔧 Using build output binary...'); await copyFile(sourcePath, binaryPath); return; } throw new Error('Build output binary not found'); } /** * Install from local development binary * @param {string} binaryPath - Destination binary path * @returns {Promise<void>} */ async installFromLocalBinary(binaryPath) { const localPath = this.platform.getLocalBinaryPath(); if (await this.fileExists(localPath)) { console.log('🔧 Using local development binary...'); await copyFile(localPath, binaryPath); return; } throw new Error('Local development binary not found'); } /** * Install from download * @param {string} binaryPath - Destination binary path * @returns {Promise<void>} */ async installFromDownload(binaryPath) { console.log('📥 Downloading binary...'); await this.downloadBinary(binaryPath); } /** * Download binary from GitHub releases * @param {string} binaryPath - Path where to save the binary * @returns {Promise<void>} */ async downloadBinary(binaryPath) { const url = this.platform.getDownloadUrl(this.version); console.log(`📥 Downloading from: ${url}`); // Add to temp files for cleanup this.tempFiles.push(binaryPath); return new Promise((resolve, reject) => { const file = fs.createWriteStream(binaryPath); let downloadStartTime = Date.now(); const request = https.get(url, (response) => { // Handle redirects if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { console.log(`📍 Redirecting to: ${response.headers.location}`); file.close(); https.get(response.headers.location, (redirectResponse) => { if (redirectResponse.statusCode === 200) { const newFile = fs.createWriteStream(binaryPath); this.setupDownloadProgress(redirectResponse, newFile, downloadStartTime); redirectResponse.pipe(newFile); newFile.on('finish', () => { console.log('\n✅ Download completed'); newFile.close(); resolve(); }); newFile.on('error', (err) => { this.cleanup([binaryPath]); reject(new Error(`File write error: ${err.message}`)); }); } else { reject(new Error(`Download failed with status: ${redirectResponse.statusCode}`)); } }).on('error', reject); return; } if (response.statusCode === 200) { this.setupDownloadProgress(response, file, downloadStartTime); response.pipe(file); } else if (response.statusCode === 404) { reject(new Error(`Binary not found for version ${this.version}. Please check if the release exists.`)); } else { reject(new Error(`Download failed with status: ${response.statusCode}`)); } }); request.setTimeout(30000, () => { request.abort(); reject(new Error('Download request timed out')); }); request.on('error', (err) => { reject(new Error(`Download request failed: ${err.message}`)); }); file.on('finish', () => { console.log('\n✅ Download completed'); file.close(); // Remove from temp files since download succeeded this.tempFiles = this.tempFiles.filter(f => f !== binaryPath); resolve(); }); file.on('error', (err) => { this.cleanup([binaryPath]); reject(new Error(`File write error: ${err.message}`)); }); }); } /** * Setup download progress reporting * @param {http.IncomingMessage} response - HTTP response * @param {fs.WriteStream} file - File stream * @param {number} startTime - Download start time */ setupDownloadProgress(response, file, startTime) { let downloadedBytes = 0; const totalBytes = parseInt(response.headers['content-length'] || '0'); let lastUpdate = 0; response.on('data', (chunk) => { downloadedBytes += chunk.length; const now = Date.now(); // Update progress every 200ms to avoid spam if (now - lastUpdate > 200) { lastUpdate = now; if (totalBytes > 0) { const progress = Math.round((downloadedBytes / totalBytes) * 100); const elapsed = (now - startTime) / 1000; const speed = downloadedBytes / elapsed; const speedStr = this.formatBytes(speed); const totalStr = this.formatBytes(totalBytes); const downloadedStr = this.formatBytes(downloadedBytes); process.stdout.write(`\r📥 Downloading... ${progress}% (${downloadedStr}/${totalStr}) @ ${speedStr}/s`); } else { const downloadedStr = this.formatBytes(downloadedBytes); process.stdout.write(`\r📥 Downloading... ${downloadedStr}`); } } }); } /** * Format bytes for human-readable display * @param {number} bytes - Number of bytes * @returns {string} Formatted string */ formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } /** * Make binary executable * @param {string} binaryPath - Path to the binary * @returns {Promise<void>} */ async makeExecutable(binaryPath) { if (this.platform.isWindows()) { // Windows doesn't need chmod return; } try { await chmod(binaryPath, 0o755); console.log('✅ Binary made executable'); } catch (error) { throw new Error(`Failed to make binary executable: ${error.message}`); } } /** * Test if the binary works * @param {string} binaryPath - Path to the binary * @returns {Promise<boolean>} */ async testBinary(binaryPath) { return new Promise((resolve) => { const child = spawn(binaryPath, ['version'], { stdio: 'pipe', timeout: 10000 }); let output = ''; let errorOutput = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { errorOutput += data.toString(); }); child.on('close', (code) => { // Check if command executed successfully and output contains version info const success = code === 0 && (output.includes('lsp-gateway') || output.includes('version') || output.includes('dev') || output.trim().length > 0); if (success) { console.log('✅ Binary test successful'); } else { console.log(`⚠️ Binary test failed: code=${code}, output="${output}", error="${errorOutput}"`); } resolve(success); }); child.on('error', (error) => { console.log(`⚠️ Binary test error: ${error.message}`); resolve(false); }); }); } /** * Show quick start information */ async showQuickStart() { console.log('🚀 Quick Start:'); console.log(' lsp-gateway server # Start with default config'); console.log(' lsp-gateway --help # Show help'); console.log(' npm run check-servers # Check LSP server availability'); console.log(' npm run create-config # Create default config'); console.log(''); console.log('📚 Documentation: https://github.com/lsp-gateway/lsp-gateway'); console.log(''); } /** * Show post-installation information and recommendations */ async showPostInstallInfo() { console.log('📝 Next Steps:'); console.log(''); console.log('1. Install Language Servers (as needed):'); console.log(' • Go: go install golang.org/x/tools/gopls@latest'); console.log(' • Python: pip install jedi-language-server'); console.log(' • TypeScript/JavaScript: npm install -g typescript-language-server'); console.log(' • Java: Download Eclipse JDT Language Server'); console.log(''); console.log('2. Create configuration file:'); console.log(' npm run create-config'); console.log(''); console.log('3. Check LSP server availability:'); console.log(' npm run check-servers'); console.log(''); console.log('4. Start the gateway:'); console.log(' lsp-gateway server'); console.log(''); console.log('💡 Tips:'); console.log(' • Use lsp-gateway --help for all available options'); console.log(' • The server runs on port 8080 by default'); console.log(' • LSP requests are sent to http://localhost:8080/jsonrpc'); console.log(' • Supports Go, Python, TypeScript, JavaScript, and Java'); } /** * Handle installation errors with comprehensive troubleshooting * @param {Error} error - The error that occurred */ async handleInstallationError(error) { console.error('❌ Installation failed:', error.message); console.error(''); console.error('📝 Troubleshooting:'); console.error(' 1. Check that your platform is supported'); console.error(' 2. Ensure you have internet connectivity'); console.error(' 3. Try running: npm install --force'); console.error(' 4. Check the GitHub releases page for manual download'); console.error(' 5. Verify file permissions in the installation directory'); console.error(''); console.error('Platform Info:'); console.error(` Current: ${this.platform.getCurrentPlatformString()}`); console.error(` Supported: ${this.platform.getSupportedPlatforms().join(', ')}`); console.error(''); console.error('Environment:'); console.error(` Node.js: ${process.version}`); console.error(` OS: ${process.platform} ${process.arch}`); console.error(` Working Directory: ${process.cwd()}`); console.error(''); // Try to provide specific help based on the error if (error.message.includes('EACCES') || error.message.includes('permission')) { console.error('💡 Permission Error Solutions:'); console.error(' • Run with sudo (Linux/macOS): sudo npm install'); console.error(' • Run as Administrator (Windows)'); console.error(' • Check directory permissions'); console.error(''); } if (error.message.includes('ENOTFOUND') || error.message.includes('network')) { console.error('💡 Network Error Solutions:'); console.error(' • Check your internet connection'); console.error(' • Try using a different network'); console.error(' • Check if GitHub is accessible'); console.error(''); } if (error.message.includes('checksum verification failed')) { console.error('🔐 Security Error - Checksum Verification Failed:'); console.error(' • The downloaded binary failed integrity verification'); console.error(' • This could indicate a man-in-the-middle attack or corrupted download'); console.error(' • Try downloading again from a different network'); console.error(' • Verify your network connection is secure (use HTTPS)'); console.error(' • Contact the maintainers if the problem persists'); console.error(' • For development only: SKIP_CHECKSUM_VERIFICATION=true (NOT RECOMMENDED)'); console.error(''); } // Don't fail the installation completely - allow manual setup console.log('⚠️ You can manually place the binary in the bin/ directory'); console.log(' or build from source using: make build'); process.exit(0); } /** * Ensure a directory exists * @param {string} dirPath - Directory path */ async ensureDirectory(dirPath) { try { await mkdir(dirPath, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { throw error; } } } /** * Check if a file exists * @param {string} filePath - File path * @returns {Promise<boolean>} */ async fileExists(filePath) { try { await access(filePath); return true; } catch { return false; } } /** * Cleanup temporary files * @param {Array<string>} files - Array of file paths to cleanup */ async cleanup(files) { if (!files || files.length === 0) return; for (const file of files) { try { if (await this.fileExists(file)) { await unlink(file); } } catch (error) { // Ignore cleanup errors } } } } // Run installer if this script is executed directly if (require.main === module) { const installer = new Installer(); // Handle process termination gracefully process.on('SIGINT', async () => { console.log('\n⚠️ Installation interrupted'); await installer.cleanup(installer.tempFiles); process.exit(1); }); process.on('SIGTERM', async () => { console.log('\n⚠️ Installation terminated'); await installer.cleanup(installer.tempFiles); process.exit(1); }); installer.install().catch(async (error) => { console.error('Installation failed:', error); await installer.cleanup(installer.tempFiles); process.exit(1); }); } module.exports = Installer;