advanced-live-server-installer
Version:
Auto-installer for Advanced Live Server VS Code Extension
513 lines (507 loc) • 21.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OfflineCloudService = void 0;
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const crypto = __importStar(require("crypto"));
const child_process = __importStar(require("child_process"));
class OfflineCloudService {
constructor(context) {
this.resources = new Map();
this.isRunning = false;
this.context = context;
this.outputChannel = vscode.window.createOutputChannel('Advanced Live Server - Offline Cloud');
this.config = this.loadConfig();
this.analytics = this.initializeAnalytics();
}
loadConfig() {
const workspaceConfig = vscode.workspace.getConfiguration('advancedLiveServer.offlineCloud');
return {
enabled: workspaceConfig.get('enabled', false),
port: workspaceConfig.get('port', 8443),
domain: workspaceConfig.get('domain', 'localhost'),
ssl: {
enabled: workspaceConfig.get('ssl.enabled', true),
certPath: workspaceConfig.get('ssl.certPath', ''),
keyPath: workspaceConfig.get('ssl.keyPath', ''),
},
features: {
cdn: workspaceConfig.get('features.cdn', true),
analytics: workspaceConfig.get('features.analytics', true),
caching: workspaceConfig.get('features.caching', true),
compression: workspaceConfig.get('features.compression', true),
security: workspaceConfig.get('features.security', true),
},
storage: {
maxSize: workspaceConfig.get('storage.maxSize', 100),
cleanupInterval: workspaceConfig.get('storage.cleanupInterval', 24),
},
};
}
initializeAnalytics() {
return {
requests: 0,
bandwidth: 0,
errors: 0,
uniqueVisitors: 0,
topResources: [],
responseTimes: [],
};
}
async start() {
if (this.isRunning) {
throw new Error('Offline cloud service is already running');
}
if (!this.config.enabled) {
throw new Error('Offline cloud service is not enabled');
}
this.outputChannel.appendLine('Starting offline cloud service...');
try {
// Create SSL certificates if needed
if (this.config.ssl.enabled) {
await this.generateSSLCertificates();
}
// Initialize storage
await this.initializeStorage();
// Start server
if (this.config.ssl.enabled) {
this.server = https.createServer({
cert: fs.readFileSync(this.config.ssl.certPath),
key: fs.readFileSync(this.config.ssl.keyPath),
}, this.handleRequest.bind(this));
}
else {
this.server = http.createServer(this.handleRequest.bind(this));
}
this.server.listen(this.config.port, () => {
this.isRunning = true;
const protocol = this.config.ssl.enabled ? 'https' : 'http';
const url = `${protocol}://${this.config.domain}:${this.config.port}`;
this.outputChannel.appendLine(`✅ Offline cloud service started at ${url}`);
vscode.window
.showInformationMessage(`Offline cloud service started at ${url}`, 'Open in Browser')
.then(selection => {
if (selection === 'Open in Browser') {
vscode.env.openExternal(vscode.Uri.parse(url));
}
});
});
// Start cleanup interval
this.startCleanupInterval();
}
catch (error) {
this.outputChannel.appendLine(`❌ Failed to start offline cloud service: ${error}`);
throw error;
}
}
async stop() {
if (!this.isRunning) {
return;
}
this.outputChannel.appendLine('Stopping offline cloud service...');
if (this.server) {
this.server.close();
this.server = undefined;
}
this.isRunning = false;
this.outputChannel.appendLine('✅ Offline cloud service stopped');
}
async generateSSLCertificates() {
const certDir = path.join(this.context.globalStorageUri.fsPath, 'ssl');
if (!fs.existsSync(certDir)) {
fs.mkdirSync(certDir, { recursive: true });
}
this.config.ssl.certPath = path.join(certDir, 'cert.pem');
this.config.ssl.keyPath = path.join(certDir, 'key.pem');
// Generate self-signed certificate if it doesn't exist
if (!fs.existsSync(this.config.ssl.certPath) ||
!fs.existsSync(this.config.ssl.keyPath)) {
this.outputChannel.appendLine('Generating SSL certificates...');
const certScript = `
const forge = require('node-forge');
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: 'commonName', value: '${this.config.domain}' },
{ name: 'countryName', value: 'US' },
{ shortName: 'ST', value: 'State' },
{ name: 'localityName', value: 'City' },
{ name: 'organizationName', value: 'Advanced Live Server' },
{ shortName: 'OU', value: 'Development' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey);
const certPem = forge.pki.certificateToPem(cert);
const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
require('fs').writeFileSync('${this.config.ssl.certPath}', certPem);
require('fs').writeFileSync('${this.config.ssl.keyPath}', keyPem);
`;
const scriptPath = path.join(this.context.globalStorageUri.fsPath, 'generate-cert.js');
fs.writeFileSync(scriptPath, certScript);
return new Promise((resolve, reject) => {
child_process.exec(`node ${scriptPath}`, error => {
if (error) {
reject(new Error(`Failed to generate SSL certificates: ${error.message}`));
}
else {
this.outputChannel.appendLine('✅ SSL certificates generated');
resolve();
}
});
});
}
}
async initializeStorage() {
const storageDir = path.join(this.context.globalStorageUri.fsPath, 'cloud-storage');
if (!fs.existsSync(storageDir)) {
fs.mkdirSync(storageDir, { recursive: true });
}
// Create subdirectories
const subdirs = ['cdn', 'cache', 'uploads', 'analytics'];
for (const dir of subdirs) {
const dirPath = path.join(storageDir, dir);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// Load existing resources
await this.scanStorage();
}
async scanStorage() {
const storageDir = path.join(this.context.globalStorageUri.fsPath, 'cloud-storage');
const scanDirectory = (dir, prefix = '') => {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
const resourcePath = prefix + '/' + item.name;
if (item.isDirectory()) {
scanDirectory(fullPath, resourcePath);
}
else {
const stats = fs.statSync(fullPath);
const content = fs.readFileSync(fullPath);
const etag = crypto.createHash('md5').update(content).digest('hex');
const resource = {
id: crypto.randomUUID(),
path: resourcePath,
type: 'file',
size: stats.size,
mimeType: this.getMimeType(item.name),
lastModified: stats.mtime,
etag: etag,
cacheControl: 'public, max-age=3600',
headers: {},
};
this.resources.set(resourcePath, resource);
}
}
};
scanDirectory(storageDir);
}
getMimeType(filename) {
const ext = path.extname(filename).toLowerCase();
const mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
};
return mimeTypes[ext] || 'application/octet-stream';
}
handleRequest(req, res) {
const startTime = Date.now();
const url = req.url || '/';
// Update analytics
this.analytics.requests++;
try {
// Security headers
if (this.config.features.security) {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
// Handle different request types
if (req.method === 'GET') {
this.handleGetRequest(url, req, res);
}
else if (req.method === 'POST') {
this.handlePostRequest(url, req, res);
}
else if (req.method === 'PUT') {
this.handlePutRequest(url, req, res);
}
else if (req.method === 'DELETE') {
this.handleDeleteRequest(url, req, res);
}
else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
}
// Update response time analytics
const responseTime = Date.now() - startTime;
this.analytics.responseTimes.push(responseTime);
if (this.analytics.responseTimes.length > 100) {
this.analytics.responseTimes.shift();
}
}
catch {
this.analytics.errors++;
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}
handleGetRequest(url, req, res) {
// Check cache first
if (this.config.features.caching) {
const cached = this.getCachedResource();
if (cached) {
res.writeHead(200, cached.headers);
res.end(cached.content);
return;
}
}
// Handle API endpoints
if (url.startsWith('/api/')) {
this.handleAPIRequest(url, req, res);
return;
}
// Handle static files
const resource = this.resources.get(url);
if (resource) {
const filePath = path.join(this.context.globalStorageUri.fsPath, 'cloud-storage', resource.path);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath);
// Apply compression if enabled
if (this.config.features.compression &&
this.isCompressible(resource.mimeType)) {
const zlib = require('zlib');
const compressed = zlib.gzipSync(content);
res.writeHead(200, {
'Content-Type': resource.mimeType,
'Content-Encoding': 'gzip',
'Content-Length': compressed.length,
ETag: resource.etag,
'Cache-Control': resource.cacheControl,
});
res.end(compressed);
}
else {
res.writeHead(200, {
'Content-Type': resource.mimeType,
'Content-Length': content.length,
ETag: resource.etag,
'Cache-Control': resource.cacheControl,
});
res.end(content);
}
// Update bandwidth analytics
this.analytics.bandwidth += content.length;
// Cache the resource
if (this.config.features.caching) {
this.cacheResource();
}
}
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Resource not found' }));
}
}
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Resource not found' }));
}
}
handleAPIRequest(url, req, res) {
if (url === '/api/analytics') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(this.analytics));
}
else if (url === '/api/resources') {
const resources = Array.from(this.resources.values());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(resources));
}
else if (url.startsWith('/api/upload')) {
this.handleUpload(req, res);
}
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'API endpoint not found' }));
}
}
handlePostRequest(url, req, res) {
if (url.startsWith('/api/upload')) {
this.handleUpload(req, res);
}
else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
}
}
handlePutRequest(url, req, res) {
// Handle file updates
res.writeHead(501, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not implemented' }));
}
handleDeleteRequest(url, req, res) {
// Handle file deletion
res.writeHead(501, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not implemented' }));
}
handleUpload(req, res) {
// Simple file upload handling
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
const { filename, content, type } = data;
const uploadDir = path.join(this.context.globalStorageUri.fsPath, 'cloud-storage', 'uploads');
const filePath = path.join(uploadDir, filename);
fs.writeFileSync(filePath, Buffer.from(content, 'base64'));
const stats = fs.statSync(filePath);
const etag = crypto.createHash('md5').update(content).digest('hex');
const resource = {
id: crypto.randomUUID(),
path: '/uploads/' + filename,
type: 'file',
size: stats.size,
mimeType: type || this.getMimeType(filename),
lastModified: stats.mtime,
etag: etag,
cacheControl: 'public, max-age=3600',
headers: {},
};
this.resources.set(resource.path, resource);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, resource }));
}
catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid upload data' }));
}
});
}
isCompressible(mimeType) {
const compressibleTypes = [
'text/',
'application/json',
'application/javascript',
'application/xml',
'application/xhtml+xml',
];
return compressibleTypes.some(type => mimeType.startsWith(type));
}
getCachedResource() {
// Simple in-memory cache implementation
return null; // Placeholder
}
cacheResource() {
// Simple in-memory cache implementation
// In a real implementation, you'd use a proper caching mechanism
}
startCleanupInterval() {
setInterval(() => {
this.cleanupStorage();
}, this.config.storage.cleanupInterval * 60 * 60 * 1000); // Convert hours to milliseconds
}
cleanupStorage() {
this.outputChannel.appendLine('Running storage cleanup...');
const storageDir = path.join(this.context.globalStorageUri.fsPath, 'cloud-storage');
let totalSize = 0;
const calculateSize = (dir) => {
let size = 0;
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
size += calculateSize(fullPath);
}
else {
size += fs.statSync(fullPath).size;
}
}
return size;
};
totalSize = calculateSize(storageDir);
const maxSizeBytes = this.config.storage.maxSize * 1024 * 1024; // Convert MB to bytes
if (totalSize > maxSizeBytes) {
this.outputChannel.appendLine(`Storage cleanup needed: ${totalSize} bytes > ${maxSizeBytes} bytes`);
// Implement cleanup logic here
}
}
getAnalytics() {
return { ...this.analytics };
}
getResources() {
return Array.from(this.resources.values());
}
isServiceRunning() {
return this.isRunning;
}
getServiceUrl() {
const protocol = this.config.ssl.enabled ? 'https' : 'http';
return `${protocol}://${this.config.domain}:${this.config.port}`;
}
updateConfig(updates) {
this.config = { ...this.config, ...updates };
// Update workspace configuration
const config = vscode.workspace.getConfiguration('advancedLiveServer.offlineCloud');
if (updates.enabled !== undefined) {
config.update('enabled', updates.enabled);
}
if (updates.port !== undefined) {
config.update('port', updates.port);
}
if (updates.domain !== undefined) {
config.update('domain', updates.domain);
}
}
}
exports.OfflineCloudService = OfflineCloudService;
//# sourceMappingURL=offline-cloud.js.map