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>
500 lines
No EOL
14 KiB
Go
500 lines
No EOL
14 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/vibetunnel/linux/pkg/ngrok"
|
|
"github.com/vibetunnel/linux/pkg/session"
|
|
)
|
|
|
|
type Server struct {
|
|
manager *session.Manager
|
|
staticPath string
|
|
password string
|
|
ngrokService *ngrok.Service
|
|
port int
|
|
}
|
|
|
|
func NewServer(manager *session.Manager, staticPath, password string, port int) *Server {
|
|
return &Server{
|
|
manager: manager,
|
|
staticPath: staticPath,
|
|
password: password,
|
|
ngrokService: ngrok.NewService(),
|
|
port: port,
|
|
}
|
|
}
|
|
|
|
func (s *Server) Start(addr string) error {
|
|
handler := s.createHandler()
|
|
return http.ListenAndServe(addr, handler)
|
|
}
|
|
|
|
func (s *Server) createHandler() http.Handler {
|
|
r := mux.NewRouter()
|
|
|
|
api := r.PathPrefix("/api").Subrouter()
|
|
if s.password != "" {
|
|
api.Use(s.basicAuthMiddleware)
|
|
}
|
|
|
|
api.HandleFunc("/health", s.handleHealth).Methods("GET")
|
|
api.HandleFunc("/sessions", s.handleListSessions).Methods("GET")
|
|
api.HandleFunc("/sessions", s.handleCreateSession).Methods("POST")
|
|
api.HandleFunc("/sessions/{id}", s.handleGetSession).Methods("GET")
|
|
api.HandleFunc("/sessions/{id}/stream", s.handleStreamSession).Methods("GET")
|
|
api.HandleFunc("/sessions/{id}/snapshot", s.handleSnapshotSession).Methods("GET")
|
|
api.HandleFunc("/sessions/{id}/input", s.handleSendInput).Methods("POST")
|
|
api.HandleFunc("/sessions/{id}", s.handleKillSession).Methods("DELETE")
|
|
api.HandleFunc("/sessions/{id}/cleanup", s.handleCleanupSession).Methods("DELETE")
|
|
api.HandleFunc("/sessions/{id}/resize", s.handleResizeSession).Methods("POST")
|
|
api.HandleFunc("/sessions/multistream", s.handleMultistream).Methods("GET")
|
|
api.HandleFunc("/cleanup-exited", s.handleCleanupExited).Methods("POST")
|
|
api.HandleFunc("/fs/browse", s.handleBrowseFS).Methods("GET")
|
|
api.HandleFunc("/mkdir", s.handleMkdir).Methods("POST")
|
|
|
|
// Ngrok endpoints
|
|
api.HandleFunc("/ngrok/start", s.handleNgrokStart).Methods("POST")
|
|
api.HandleFunc("/ngrok/stop", s.handleNgrokStop).Methods("POST")
|
|
api.HandleFunc("/ngrok/status", s.handleNgrokStatus).Methods("GET")
|
|
|
|
if s.staticPath != "" {
|
|
r.PathPrefix("/").Handler(http.FileServer(http.Dir(s.staticPath)))
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (s *Server) basicAuthMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth == "" {
|
|
s.unauthorized(w)
|
|
return
|
|
}
|
|
|
|
const prefix = "Basic "
|
|
if !strings.HasPrefix(auth, prefix) {
|
|
s.unauthorized(w)
|
|
return
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
|
if err != nil {
|
|
s.unauthorized(w)
|
|
return
|
|
}
|
|
|
|
parts := strings.SplitN(string(decoded), ":", 2)
|
|
if len(parts) != 2 || parts[0] != "admin" || parts[1] != s.password {
|
|
s.unauthorized(w)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *Server) unauthorized(w http.ResponseWriter) {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="VibeTunnel"`)
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
}
|
|
|
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) handleListSessions(w http.ResponseWriter, r *http.Request) {
|
|
sessions, err := s.manager.ListSessions()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(sessions)
|
|
}
|
|
|
|
func (s *Server) handleCreateSession(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Command []string `json:"command"` // Rust API format
|
|
WorkingDir string `json:"workingDir"` // Rust API format
|
|
Width int `json:"width"` // Terminal width
|
|
Height int `json:"height"` // Terminal height
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body. Expected JSON with 'command' array and optional 'workingDir'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req.Command) == 0 {
|
|
http.Error(w, "Command array is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
cmdline := req.Command
|
|
cwd := req.WorkingDir
|
|
|
|
// Set default terminal dimensions if not provided
|
|
width := req.Width
|
|
if width <= 0 {
|
|
width = 120 // Better default for modern terminals
|
|
}
|
|
height := req.Height
|
|
if height <= 0 {
|
|
height = 30 // Better default for modern terminals
|
|
}
|
|
|
|
// Expand ~ in working directory
|
|
if cwd != "" && cwd[0] == '~' {
|
|
if cwd == "~" || cwd[:2] == "~/" {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err == nil {
|
|
if cwd == "~" {
|
|
cwd = homeDir
|
|
} else {
|
|
cwd = filepath.Join(homeDir, cwd[2:])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sess, err := s.manager.CreateSession(session.Config{
|
|
Name: req.Name,
|
|
Cmdline: cmdline,
|
|
Cwd: cwd,
|
|
Width: width,
|
|
Height: height,
|
|
})
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Session created successfully",
|
|
"error": nil,
|
|
"sessionId": sess.ID,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleGetSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Return current session info without blocking on status update
|
|
// Status will be eventually consistent through background updates
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(sess)
|
|
}
|
|
|
|
func (s *Server) handleStreamSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
streamer := NewSSEStreamer(w, sess)
|
|
streamer.Stream()
|
|
}
|
|
|
|
func (s *Server) handleSnapshotSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
snapshot, err := GetSessionSnapshot(sess)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(snapshot)
|
|
}
|
|
|
|
func (s *Server) handleSendInput(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
log.Printf("[ERROR] handleSendInput: Session %s not found", vars["id"])
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Input string `json:"input"`
|
|
Text string `json:"text"` // Alternative field name
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Printf("[ERROR] handleSendInput: Failed to decode request: %v", err)
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Handle alternative field names for compatibility
|
|
input := req.Input
|
|
if input == "" && req.Text != "" {
|
|
input = req.Text
|
|
}
|
|
|
|
// Define special keys exactly as in Swift/macOS version
|
|
specialKeys := map[string]string{
|
|
"arrow_up": "\x1b[A",
|
|
"arrow_down": "\x1b[B",
|
|
"arrow_right": "\x1b[C",
|
|
"arrow_left": "\x1b[D",
|
|
"escape": "\x1b",
|
|
"enter": "\r", // CR, not LF (to match Swift)
|
|
"ctrl_enter": "\r", // CR for ctrl+enter
|
|
"shift_enter": "\x1b\x0d", // ESC + CR for shift+enter
|
|
}
|
|
|
|
// Check if this is a special key (automatic detection like Swift version)
|
|
if mappedKey, isSpecialKey := specialKeys[input]; isSpecialKey {
|
|
log.Printf("[DEBUG] handleSendInput: Sending special key '%s' (%q) to session %s", input, mappedKey, sess.ID[:8])
|
|
err = sess.SendKey(mappedKey)
|
|
} else {
|
|
log.Printf("[DEBUG] handleSendInput: Sending text '%s' to session %s", input, sess.ID[:8])
|
|
err = sess.SendText(input)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] handleSendInput: Failed to send input: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Printf("[DEBUG] handleSendInput: Successfully sent input to session %s", sess.ID[:8])
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleKillSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if err := sess.Kill(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Session deleted successfully",
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleCleanupSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
if err := s.manager.RemoveSession(vars["id"]); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleCleanupExited(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.manager.CleanupExitedSessions(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleMultistream(w http.ResponseWriter, r *http.Request) {
|
|
sessionIDs := r.URL.Query()["session_id"]
|
|
if len(sessionIDs) == 0 {
|
|
http.Error(w, "No session IDs provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
streamer := NewMultiSSEStreamer(w, s.manager, sessionIDs)
|
|
streamer.Stream()
|
|
}
|
|
|
|
func (s *Server) handleBrowseFS(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Query().Get("path")
|
|
if path == "" {
|
|
path = "."
|
|
}
|
|
|
|
entries, err := BrowseDirectory(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(entries)
|
|
}
|
|
|
|
func (s *Server) handleMkdir(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := os.MkdirAll(req.Path, 0755); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleResizeSession(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
sess, err := s.manager.GetSession(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Session not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Width <= 0 || req.Height <= 0 {
|
|
http.Error(w, "Width and height must be positive integers", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := sess.Resize(req.Width, req.Height); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Session resized successfully",
|
|
"width": req.Width,
|
|
"height": req.Height,
|
|
})
|
|
}
|
|
|
|
// Ngrok Handlers
|
|
|
|
func (s *Server) handleNgrokStart(w http.ResponseWriter, r *http.Request) {
|
|
var req ngrok.StartRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.AuthToken == "" {
|
|
http.Error(w, "Auth token is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if ngrok is already running
|
|
if s.ngrokService.IsRunning() {
|
|
status := s.ngrokService.GetStatus()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Ngrok tunnel is already running",
|
|
"tunnel": status,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Start the tunnel
|
|
if err := s.ngrokService.Start(req.AuthToken, s.port); err != nil {
|
|
log.Printf("[ERROR] Failed to start ngrok tunnel: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Return immediate response - tunnel status will be updated asynchronously
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Ngrok tunnel is starting",
|
|
"tunnel": s.ngrokService.GetStatus(),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleNgrokStop(w http.ResponseWriter, r *http.Request) {
|
|
if !s.ngrokService.IsRunning() {
|
|
http.Error(w, "Ngrok tunnel is not running", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := s.ngrokService.Stop(); err != nil {
|
|
log.Printf("[ERROR] Failed to stop ngrok tunnel: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "Ngrok tunnel stopped",
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleNgrokStatus(w http.ResponseWriter, r *http.Request) {
|
|
status := s.ngrokService.GetStatus()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"tunnel": status,
|
|
})
|
|
}
|
|
|
|
// StartNgrok is a convenience method for CLI integration
|
|
func (s *Server) StartNgrok(authToken string) error {
|
|
return s.ngrokService.Start(authToken, s.port)
|
|
}
|
|
|
|
// StopNgrok is a convenience method for CLI integration
|
|
func (s *Server) StopNgrok() error {
|
|
return s.ngrokService.Stop()
|
|
}
|
|
|
|
// GetNgrokStatus returns the current ngrok status
|
|
func (s *Server) GetNgrokStatus() ngrok.StatusResponse {
|
|
return s.ngrokService.GetStatus()
|
|
} |