mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
* Add Linux implementation of VibeTunnel This commit introduces a complete Linux port of VibeTunnel, providing feature parity with the macOS version. The implementation includes: - Full Go-based server with identical REST API and WebSocket endpoints - Terminal session management using PTY (pseudo-terminal) handling - Asciinema recording format for session playback - Compatible CLI interface matching the macOS `vt` command - Support for all VibeTunnel features: password protection, network modes, ngrok integration - Comprehensive build system with Makefile supporting various installation methods - Systemd service integration for running as a system daemon The Linux version maintains 100% compatibility with the existing web UI and can be used as a drop-in replacement for the macOS app on Linux systems. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add comprehensive ngrok integration to Linux VibeTunnel Implements full ngrok tunnel support for the Go/Linux version to match the macOS Swift implementation, enabling secure public access to local VibeTunnel instances. - **ngrok Service**: Complete lifecycle management with status tracking - **HTTP API**: RESTful endpoints matching macOS version - **CLI Support**: Command-line ngrok flags and integration - **Auto-forwarding**: Built-in HTTP request forwarding to local server - `POST /api/ngrok/start` - Start tunnel with auth token - `POST /api/ngrok/stop` - Stop active tunnel - `GET /api/ngrok/status` - Get current tunnel status - Uses `golang.ngrok.com/ngrok` SDK for native Go integration - Thread-safe service with mutex protection - Comprehensive error handling and logging - Real-time status updates (disconnected/connecting/connected/error) - Proper context cancellation for graceful shutdown ```bash vibetunnel --serve --ngrok --ngrok-token "your_token" vibetunnel --serve --port 4030 --ngrok --ngrok-token "your_token" ``` - Added golang.ngrok.com/ngrok v1.13.0 - Updated web packages (security fixes for puppeteer) Maintains full API compatibility with macOS VibeTunnel for seamless cross-platform operation and consistent web frontend integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * up * Fix SSE streaming performance with byte-based approach Addresses @badlogic's review feedback to prevent performance issues with line-based file reading in processNewContent(). ## Changes Made ### Performance Fix - **Byte-based seeking**: Replace line counting with file position tracking - **Efficient reads**: Only read new content since last position using file.Seek() - **Memory optimization**: Avoid reading entire file on each update - **Incomplete line handling**: Properly handle partial lines at file end ### Technical Details - Changed lastLineCount *int → seenBytes *int64 - Use file.Seek(seenBytes, 0) to jump to last read position - Read only new content with currentSize - seenBytes - Handle incomplete lines by adjusting seek position - Maintains same functionality with better performance ### Benefits - **Scalability**: No longer reads entire file for each update - **Performance**: O(new_content) instead of O(total_content) - **Memory**: Constant memory usage regardless of file size - **Reliability**: Handles concurrent writes and partial lines correctly This prevents the SSE streaming from exploding in our faces as @badlogic warned, especially for long-running sessions with large output files. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Optimize streaming performance to reduce 1+ second delays Implements multiple optimizations to address user-reported 1+ second delay between typing and stream rendering: ## PTY Reading Optimizations - **Reduced sleep times**: 100ms → 1ms for EOF checks - **Faster polling**: 10ms → 1ms for zero-byte reads - **FIFO optimization**: 1s → 100ms for stdin EOF polling ## UTF-8 Buffering Improvements - **Timeout-based flushing**: 5ms timer for incomplete UTF-8 sequences - **Real-time streaming**: Don't wait for complete sequences in interactive mode - **Smart buffering**: Balance correctness with responsiveness ## File I/O Optimizations - **Immediate sync**: Call file.Sync() after each write for instant fsnotify - **Reduced SSE timeout**: 1s → 100ms for session alive checks - **Better responsiveness**: Ensure file changes trigger immediately ## Technical Changes - Added StreamWriter.scheduleFlush() with 5ms timeout - Enhanced writeEvent() with conditional file syncing - Optimized PTY read/write loop timing - Improved SSE streaming frequency These changes target the main bottlenecks identified in the PTY → file → fsnotify → SSE → browser pipeline. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix critical stdin polling delay causing 1+ second input lag - Reduced FIFO EOF polling from 100ms to 1ms - Reduced EAGAIN polling from 1ms to 100µs - Added immediate continue after successful writes - This eliminates the major input delay bottleneck 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix critical performance issues causing resource leaks and CPU burns Performance optimizations based on code review feedback: 1. **Fix SSE goroutine leaks**: - Added client disconnect detection to SSE streams - Propagate write errors to detect when clients close connections - Prevents memory leaks from abandoned streaming goroutines 2. **Fix PTY busy-loop CPU burn**: - Increased sleep from 1ms to 10ms in idle scenarios - Reduces CPU wake-ups from 1000/s to 100/s (10x improvement) - Significantly reduces CPU usage when PTY is idle 3. **Multi-stream disconnect detection**: - Added error checking to multi-stream write operations - Prevents goroutine leaks in multi-session streaming These fixes address the "thing of the things" - performance\! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Standardize session creation API response format to match Rust server Changes: - Updated Go server session creation response to include success/message/error fields - Now returns: {"success": true, "message": "Session created successfully", "error": null, "sessionId": "..."} - Maintains backward compatibility with existing sessionId field - Go server already supported both input formats (cmdline/command, cwd/workingDir) This achieves protocol compatibility between Go and Rust implementations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix delete endpoint to return 200 OK with JSON response - Changed handleKillSession to return 200 OK instead of 204 No Content - Added JSON response with success/message fields for consistency - Fixes benchmark tool compatibility expecting 200 response 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Update Go server API to match Rust format exactly - Use 'command' array instead of 'cmdline' - Use 'workingDir' instead of 'cwd' - Remove compatibility shims for cleaner API - Better error messages matching Rust server 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Major performance optimizations for Go server - Remove 100ms artificial delay in session creation (-100ms per session) - Optimize PTY I/O handling with reduced polling intervals - Implement persistent stdin pipes to avoid repeated open/close - Batch file sync operations to reduce I/O overhead (5ms batching) - Remove blocking status updates from API handlers - Increase SSE session check interval from 100ms to 1s Target: Match Rust performance (60ms avg latency, 16+ ops/sec) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix O_NONBLOCK compilation issue * Add comprehensive TLS/HTTPS support with Caddy integration Features: - Optional TLS support via CLI flags (defaults to HTTP like Rust) - Self-signed certificate generation for localhost development - Let's Encrypt automatic certificate management for domains - Custom certificate support for production environments - HTTP to HTTPS redirect capability - Maintains 100% backward compatibility with Rust version Usage examples: - Default HTTP: ./vibetunnel --serve (same as Rust) - HTTPS with self-signed: ./vibetunnel --serve --tls - HTTPS with domain: ./vibetunnel --serve --tls --tls-domain example.com - HTTPS with custom certs: ./vibetunnel --serve --tls --tls-cert cert.pem --tls-key key.pem 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix terminal sizing issues and implement dynamic resize support Backend changes: - Add handleResizeSession API endpoint for dynamic terminal resizing - Implement Session.Resize() and PTY.Resize() methods with proper validation - Add session registry in Manager to track running sessions with PTY access - Fix stdin error handling to prevent session crashes on EAGAIN errors - Write resize events to asciinema stream for frontend synchronization - Update default terminal dimensions from 80x24 to 120x30 Frontend changes: - Add width/height parameters to SessionCreateData interface - Calculate appropriate terminal dimensions when creating sessions - Implement automatic resize API calls when terminal dimensions change - Add terminal-resize event dispatch for backend synchronization - Ensure resize events bubble properly for session management Fixes nvim being stuck at 80x24 by implementing proper terminal dimension management and dynamic resizing capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add client-side resize caching and Hack Nerd Font support - Implement resize request caching to prevent redundant API calls - Add debouncing to terminal resize events (250ms delay) - Replace ResizeObserver with window.resize events only to eliminate pixel-level jitter - Add Hack Nerd Font Mono as primary terminal font with Fira Code fallback - Update session creation to use conservative 120x30 defaults - Fix terminal dimension calculation in normal mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add comprehensive XTerm color and rendering enhancements - Complete 256-color palette support with CSS variables (0-255) - Enhanced XTerm configuration with proper terminal options - True xterm-compatible 16-color theme - Text attribute support: bold, italic, underline, dim, strikethrough, inverse, invisible - Cursor blinking with CSS animation - Font rendering optimizations (disabled ligatures, antialiasing) - Terminal-specific CSS styling for better rendering - Mac option key as meta, alt-click cursor movement - Selection colors and inactive selection support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
256 lines
No EOL
7.3 KiB
Go
256 lines
No EOL
7.3 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
)
|
|
|
|
// TLSConfig represents TLS configuration options
|
|
type TLSConfig struct {
|
|
Enabled bool `json:"enabled"`
|
|
Port int `json:"port"`
|
|
Domain string `json:"domain,omitempty"` // Optional domain for Let's Encrypt
|
|
SelfSigned bool `json:"self_signed"` // Use self-signed certificates
|
|
CertPath string `json:"cert_path,omitempty"` // Custom cert path
|
|
KeyPath string `json:"key_path,omitempty"` // Custom key path
|
|
AutoRedirect bool `json:"auto_redirect"` // Redirect HTTP to HTTPS
|
|
}
|
|
|
|
// TLSServer wraps the regular server with TLS capabilities
|
|
type TLSServer struct {
|
|
*Server
|
|
tlsConfig *TLSConfig
|
|
certMagic *certmagic.Config
|
|
}
|
|
|
|
// NewTLSServer creates a new TLS-enabled server
|
|
func NewTLSServer(server *Server, tlsConfig *TLSConfig) *TLSServer {
|
|
return &TLSServer{
|
|
Server: server,
|
|
tlsConfig: tlsConfig,
|
|
}
|
|
}
|
|
|
|
// StartTLS starts the server with TLS support
|
|
func (s *TLSServer) StartTLS(httpAddr, httpsAddr string) error {
|
|
if !s.tlsConfig.Enabled {
|
|
// Fall back to regular HTTP
|
|
return s.Start(httpAddr)
|
|
}
|
|
|
|
// Set up TLS configuration
|
|
tlsConfig, err := s.setupTLS()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to setup TLS: %w", err)
|
|
}
|
|
|
|
// Create HTTP handler
|
|
handler := s.setupRoutes()
|
|
|
|
// Start HTTPS server
|
|
httpsServer := &http.Server{
|
|
Addr: httpsAddr,
|
|
Handler: handler,
|
|
TLSConfig: tlsConfig,
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
log.Printf("Starting HTTPS server on %s", httpsAddr)
|
|
|
|
// Start HTTP redirect server if enabled
|
|
if s.tlsConfig.AutoRedirect && httpAddr != "" {
|
|
go s.startHTTPRedirect(httpAddr, httpsAddr)
|
|
}
|
|
|
|
// Start HTTPS server
|
|
if s.tlsConfig.SelfSigned || (s.tlsConfig.CertPath != "" && s.tlsConfig.KeyPath != "") {
|
|
return httpsServer.ListenAndServeTLS(s.tlsConfig.CertPath, s.tlsConfig.KeyPath)
|
|
} else {
|
|
// Use CertMagic for automatic certificates
|
|
return httpsServer.ListenAndServeTLS("", "")
|
|
}
|
|
}
|
|
|
|
// setupTLS configures TLS based on the provided configuration
|
|
func (s *TLSServer) setupTLS() (*tls.Config, error) {
|
|
if s.tlsConfig.SelfSigned {
|
|
return s.setupSelfSignedTLS()
|
|
}
|
|
|
|
if s.tlsConfig.CertPath != "" && s.tlsConfig.KeyPath != "" {
|
|
return s.setupCustomCertTLS()
|
|
}
|
|
|
|
if s.tlsConfig.Domain != "" {
|
|
return s.setupCertMagicTLS()
|
|
}
|
|
|
|
// Default to self-signed
|
|
return s.setupSelfSignedTLS()
|
|
}
|
|
|
|
// setupSelfSignedTLS creates a self-signed certificate
|
|
func (s *TLSServer) setupSelfSignedTLS() (*tls.Config, error) {
|
|
// Generate self-signed certificate
|
|
cert, err := s.generateSelfSignedCert()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate self-signed certificate: %w", err)
|
|
}
|
|
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
ServerName: "localhost",
|
|
MinVersion: tls.VersionTLS12,
|
|
}, nil
|
|
}
|
|
|
|
// setupCustomCertTLS loads custom certificates
|
|
func (s *TLSServer) setupCustomCertTLS() (*tls.Config, error) {
|
|
cert, err := tls.LoadX509KeyPair(s.tlsConfig.CertPath, s.tlsConfig.KeyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load custom certificates: %w", err)
|
|
}
|
|
|
|
return &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS12,
|
|
}, nil
|
|
}
|
|
|
|
// setupCertMagicTLS configures automatic certificate management
|
|
func (s *TLSServer) setupCertMagicTLS() (*tls.Config, error) {
|
|
// Set up CertMagic for automatic HTTPS
|
|
certmagic.DefaultACME.Agreed = true
|
|
certmagic.DefaultACME.Email = "admin@" + s.tlsConfig.Domain
|
|
|
|
// Configure storage path
|
|
certmagic.Default.Storage = &certmagic.FileStorage{
|
|
Path: filepath.Join("/tmp", "vibetunnel-certs"),
|
|
}
|
|
|
|
// Get certificate for domain
|
|
err := certmagic.ManageSync(context.Background(), []string{s.tlsConfig.Domain})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to obtain certificate for domain %s: %w", s.tlsConfig.Domain, err)
|
|
}
|
|
|
|
tlsConfig, err := certmagic.TLS([]string{s.tlsConfig.Domain})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create TLS config: %w", err)
|
|
}
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// generateSelfSignedCert creates a self-signed certificate for localhost
|
|
func (s *TLSServer) generateSelfSignedCert() (tls.Certificate, error) {
|
|
// Generate RSA private key
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("failed to generate private key: %w", err)
|
|
}
|
|
|
|
// Create certificate template
|
|
template := x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"VibeTunnel"},
|
|
Country: []string{"US"},
|
|
Province: []string{""},
|
|
Locality: []string{"localhost"},
|
|
StreetAddress: []string{""},
|
|
PostalCode: []string{""},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
|
DNSNames: []string{"localhost"},
|
|
}
|
|
|
|
// Generate certificate
|
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
// Encode certificate to PEM
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
|
|
// Encode private key to PEM
|
|
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyDER})
|
|
|
|
// Create TLS certificate
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("failed to create X509 key pair: %w", err)
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// startHTTPRedirect starts an HTTP server that redirects all requests to HTTPS
|
|
func (s *TLSServer) startHTTPRedirect(httpAddr, httpsAddr string) {
|
|
redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Extract host from httpsAddr for redirect
|
|
host := r.Host
|
|
if host == "" {
|
|
host = "localhost"
|
|
}
|
|
|
|
// Remove port if present and add HTTPS port
|
|
if colonIndex := len(host) - 1; host[colonIndex] == ':' {
|
|
// Remove existing port
|
|
for i := colonIndex - 1; i >= 0; i-- {
|
|
if host[i] == ':' {
|
|
host = host[:i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add HTTPS port
|
|
if s.tlsConfig.Port != 443 {
|
|
host = fmt.Sprintf("%s:%d", host, s.tlsConfig.Port)
|
|
}
|
|
|
|
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
|
|
http.Redirect(w, r, httpsURL, http.StatusPermanentRedirect)
|
|
})
|
|
|
|
server := &http.Server{
|
|
Addr: httpAddr,
|
|
Handler: redirectHandler,
|
|
}
|
|
|
|
log.Printf("Starting HTTP redirect server on %s -> HTTPS", httpAddr)
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Printf("HTTP redirect server error: %v", err)
|
|
}
|
|
}
|
|
|
|
// setupRoutes returns the configured HTTP handler (reusing existing Server logic)
|
|
func (s *TLSServer) setupRoutes() http.Handler {
|
|
// Use the existing server's router setup
|
|
return s.Server.createHandler()
|
|
} |