diff --git a/VibeTunnel-Server/AuthenticationMiddleware.swift b/VibeTunnel-Server/AuthenticationMiddleware.swift deleted file mode 100644 index 6c7bf340..00000000 --- a/VibeTunnel-Server/AuthenticationMiddleware.swift +++ /dev/null @@ -1,121 +0,0 @@ -import CryptoKit -import Foundation -import HTTPTypes -import Hummingbird -import HummingbirdCore -import Logging - -/// Custom HTTP header name for API key -extension HTTPField.Name { - static let xAPIKey = HTTPField.Name("X-API-Key")! -} - -/// Simple authentication middleware for the tunnel server -struct AuthenticationMiddleware: RouterMiddleware { - private let logger = Logger(label: "VibeTunnel.AuthMiddleware") - private let bearerPrefix = "Bearer " - - /// In production, this should be stored securely and configurable - private let validApiKeys: Set - - init() { - // Load API keys from storage - var apiKeys = APIKeyManager.loadStoredAPIKeys() - - if apiKeys.isEmpty { - // Generate a default API key for development - let defaultKey = Self.generateAPIKey() - apiKeys = [defaultKey] - APIKeyManager.saveAPIKeys(apiKeys) - logger.info("Authentication initialized with new API key: \(defaultKey)") - } else { - logger.info("Authentication initialized with \(apiKeys.count) stored API key(s)") - } - - self.validApiKeys = apiKeys - } - - init(apiKeys: Set) { - self.validApiKeys = apiKeys - } - - func handle( - _ request: Request, - context: Context, - next: (Request, Context) async throws -> Response - ) - async throws -> Response - { - // Skip authentication for health check and WebSocket upgrade - if request.uri.path == "/health" || request.headers[HTTPField.Name.upgrade] == "websocket" { - return try await next(request, context) - } - - // Check for API key in header - if let apiKey = request.headers[HTTPField.Name.xAPIKey] { - if validApiKeys.contains(apiKey) { - return try await next(request, context) - } - } - - // Check for Bearer token - if let authorization = request.headers[HTTPField.Name.authorization], - authorization.hasPrefix(bearerPrefix) - { - let token = String(authorization.dropFirst(bearerPrefix.count)) - if validApiKeys.contains(token) { - return try await next(request, context) - } - } - - // No valid authentication found - logger.warning("Unauthorized request to \(request.uri.path)") - throw HTTPError(.unauthorized, message: "Invalid or missing API key") - } - - /// Generate a secure API key - static func generateAPIKey() -> String { - let randomBytes = SymmetricKey(size: .bits256) - let data = randomBytes.withUnsafeBytes { Data($0) } - return data.base64EncodedString() - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } -} - -/// API Key management utilities -enum APIKeyManager { - static let apiKeyStorageKey = "VibeTunnel.APIKeys" - - static func loadStoredAPIKeys() -> Set { - guard let data = UserDefaults.standard.data(forKey: apiKeyStorageKey), - let keys = try? JSONDecoder().decode(Set.self, from: data) - else { - // Generate and store a default key if none exists - let defaultKey = AuthenticationMiddleware.generateAPIKey() - let keys = Set([defaultKey]) - saveAPIKeys(keys) - return keys - } - return keys - } - - static func saveAPIKeys(_ keys: Set) { - if let data = try? JSONEncoder().encode(keys) { - UserDefaults.standard.set(data, forKey: apiKeyStorageKey) - } - } - - static func addAPIKey(_ key: String) { - var keys = loadStoredAPIKeys() - keys.insert(key) - saveAPIKeys(keys) - } - - static func removeAPIKey(_ key: String) { - var keys = loadStoredAPIKeys() - keys.remove(key) - saveAPIKeys(keys) - } -} diff --git a/VibeTunnel-Server/TunnelServer.swift b/VibeTunnel-Server/TunnelServer.swift deleted file mode 100644 index 96c4a604..00000000 --- a/VibeTunnel-Server/TunnelServer.swift +++ /dev/null @@ -1,247 +0,0 @@ -import AppKit -import Combine -import Foundation -import HTTPTypes -import Hummingbird -import HummingbirdCore -import Logging -import NIOCore -import os - -// MARK: - Response Models - -/// Server info response -struct ServerInfoResponse: ResponseCodable { - let name: String - let version: String - let uptime: TimeInterval - let sessions: Int -} - -/// Main tunnel server implementation using Hummingbird -@MainActor -final class TunnelServer: ObservableObject { - private let port: Int - private let logger = Logger(label: "VibeTunnel.TunnelServer") - private var app: Application.Responder>? - private let terminalManager = TerminalManager() - - @Published var isRunning = false - @Published var lastError: Error? - @Published var connectedClients = 0 - - init(port: Int = 8_080) { - self.port = port - } - - func start() async throws { - logger.info("Starting tunnel server on port \(port)") - - do { - // Build the Hummingbird application - let app = try await buildApplication() - self.app = app - - // Start the server - try await app.run() - - await MainActor.run { - self.isRunning = true - } - } catch { - await MainActor.run { - self.lastError = error - self.isRunning = false - } - throw error - } - } - - func stop() async { - logger.info("Stopping tunnel server") - - // In Hummingbird 2.x, the application lifecycle is managed differently - // Setting app to nil will trigger cleanup when it's deallocated - self.app = nil - - await MainActor.run { - self.isRunning = false - } - } - - private func buildApplication() async throws -> Application.Responder> { - // Create router - let router = Router() - - // Add middleware - router.add(middleware: LogRequestsMiddleware(.info)) - router.add(middleware: CORSMiddleware( - allowOrigin: .all, - allowHeaders: [.contentType, .authorization], - allowMethods: [.get, .post, .delete, .options] - )) - router.add(middleware: AuthenticationMiddleware(apiKeys: APIKeyManager.loadStoredAPIKeys())) - - // Configure routes - configureRoutes(router) - - // Add WebSocket routes - // TODO: Uncomment when HummingbirdWebSocket package is added - // router.addWebSocketRoutes(terminalManager: terminalManager) - - // Create application configuration - let configuration = ApplicationConfiguration( - address: .hostname("127.0.0.1", port: port), - serverName: "VibeTunnel" - ) - - // Create and configure the application - let app = Application( - responder: router.buildResponder(), - configuration: configuration, - logger: logger - ) - - // Add cleanup task - // Start cleanup task - Task { - while !Task.isCancelled { - await terminalManager.cleanupInactiveSessions(olderThan: 30) - try? await Task.sleep(nanoseconds: 5 * 60 * 1_000_000_000) // 5 minutes - } - } - - return app - } - - private func configureRoutes(_ router: Router) { - // Health check endpoint - router.get("/health") { _, _ -> HTTPResponse.Status in - .ok - } - - // Server info endpoint - router.get("/info") { _, _ async -> ServerInfoResponse in - let sessionCount = await self.terminalManager.listSessions().count - return ServerInfoResponse( - name: "VibeTunnel", - version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0", - uptime: ProcessInfo.processInfo.systemUptime, - sessions: sessionCount - ) - } - - // Session management endpoints - let sessions = router.group("sessions") - - // List all sessions - sessions.get("/") { _, _ async -> ListSessionsResponse in - let sessions = await self.terminalManager.listSessions() - let sessionInfos = sessions.map { session in - SessionInfo( - id: session.id.uuidString, - createdAt: session.createdAt, - lastActivity: session.lastActivity, - isActive: session.isActive - ) - } - return ListSessionsResponse(sessions: sessionInfos) - } - - // Create new session - sessions.post("/") { request, context async throws -> CreateSessionResponse in - let createRequest = try await request.decode(as: CreateSessionRequest.self, context: context) - let session = try await self.terminalManager.createSession(request: createRequest) - - return CreateSessionResponse( - sessionId: session.id.uuidString, - createdAt: session.createdAt - ) - } - - // Get session info - sessions.get(":sessionId") { _, context async throws -> SessionInfo in - guard let sessionIdString = context.parameters.get("sessionId", as: String.self), - let sessionId = UUID(uuidString: sessionIdString), - let session = await self.terminalManager.getSession(id: sessionId) - else { - throw HTTPError(.notFound) - } - - return SessionInfo( - id: session.id.uuidString, - createdAt: session.createdAt, - lastActivity: session.lastActivity, - isActive: session.isActive - ) - } - - // Close session - sessions.delete(":sessionId") { _, context async throws -> HTTPResponse.Status in - guard let sessionIdString = context.parameters.get("sessionId", as: String.self), - let sessionId = UUID(uuidString: sessionIdString) - else { - throw HTTPError(.badRequest) - } - - await self.terminalManager.closeSession(id: sessionId) - return .noContent - } - - // Command execution endpoint - router.post("/execute") { request, context async throws -> CommandResponse in - let commandRequest = try await request.decode(as: CommandRequest.self, context: context) - - guard let sessionId = UUID(uuidString: commandRequest.sessionId) else { - throw HTTPError(.badRequest, message: "Invalid session ID") - } - - do { - let (output, error) = try await self.terminalManager.executeCommand( - sessionId: sessionId, - command: commandRequest.command - ) - - return CommandResponse( - sessionId: commandRequest.sessionId, - output: output.isEmpty ? nil : output, - error: error.isEmpty ? nil : error, - exitCode: nil, - timestamp: Date() - ) - } catch { - throw HTTPError(.internalServerError, message: error.localizedDescription) - } - } - } -} - -// MARK: - Integration with AppDelegate - -extension AppDelegate { - func startTunnelServer() { - Task { - do { - let port = UserDefaults.standard.integer(forKey: "serverPort") - let tunnelServer = TunnelServer(port: port > 0 ? port : 8_080) - - // Store reference if needed - // self.tunnelServer = tunnelServer - - try await tunnelServer.start() - } catch { - let logger = Logger(label: "VibeTunnel.AppDelegate") - logger.error("Failed to start tunnel server: \(error)") - - // Show error alert - await MainActor.run { - let alert = NSAlert() - alert.messageText = "Failed to Start Server" - alert.informativeText = error.localizedDescription - alert.alertStyle = .critical - alert.runModal() - } - } - } - } -} diff --git a/VibeTunnel-Server/WebSocketHandler.swift b/VibeTunnel-Server/WebSocketHandler.swift deleted file mode 100644 index 90da58f9..00000000 --- a/VibeTunnel-Server/WebSocketHandler.swift +++ /dev/null @@ -1,194 +0,0 @@ -import Foundation -import Hummingbird -import HummingbirdCore -import NIOCore - -// import NIOWebSocket // TODO: This is available in swift-nio package -import Logging - -/// WebSocket message types for terminal communication -public enum WSMessageType: String, Codable { - case connect - case command - case output - case error - case ping - case pong - case close -} - -/// WebSocket message structure -public struct WSMessage: Codable { - public let type: WSMessageType - public let sessionId: String? - public let data: String? - public let timestamp: Date - - public init(type: WSMessageType, sessionId: String? = nil, data: String? = nil) { - self.type = type - self.sessionId = sessionId - self.data = data - self.timestamp = Date() - } -} - -// TODO: Enable when HummingbirdWebSocket package is added -// /// Handles WebSocket connections for real-time terminal communication -// final class WebSocketHandler { -// private let terminalManager: TerminalManager -// private let logger = Logger(label: "VibeTunnel.WebSocketHandler") -// private var activeConnections: [UUID: WebSocketHandler.Connection] = [:] -// -// init(terminalManager: TerminalManager) { -// self.terminalManager = terminalManager -// } -// -// /// Handle incoming WebSocket connection -// func handle(ws: WebSocket, context: some RequestContext) async { -// let connectionId = UUID() -// let connection = Connection(id: connectionId, websocket: ws) -// -// await MainActor.run { -// activeConnections[connectionId] = connection -// } -// -// logger.info("WebSocket connection established: \(connectionId)") -// -// // Set up message handlers -// ws.onText { [weak self] ws, text in -// await self?.handleTextMessage(text, connection: connection) -// } -// -// ws.onBinary { [weak self] ws, buffer in -// // Handle binary data if needed -// self?.logger.debug("Received binary data: \(buffer.readableBytes) bytes") -// } -// -// ws.onClose { [weak self] closeCode in -// await self?.handleClose(connection: connection) -// } -// -// // Send initial connection acknowledgment -// await sendMessage(WSMessage(type: .connect, data: "Connected to VibeTunnel"), to: connection) -// -// // Keep connection alive with periodic pings -// Task { -// while !Task.isCancelled && !connection.isClosed { -// await sendMessage(WSMessage(type: .ping), to: connection) -// try? await Task.sleep(nanoseconds: 30 * 1_000_000_000) // 30 seconds -// } -// } -// } -// -// private func handleTextMessage(_ text: String, connection: Connection) async { -// guard let data = text.data(using: .utf8), -// let message = try? JSONDecoder().decode(WSMessage.self, from: data) else { -// logger.error("Failed to decode WebSocket message: \(text)") -// await sendError("Invalid message format", to: connection) -// return -// } -// -// switch message.type { -// case .connect: -// // Handle session connection -// if let sessionId = message.sessionId, -// let uuid = UUID(uuidString: sessionId) { -// connection.sessionId = uuid -// await sendMessage(WSMessage(type: .output, sessionId: sessionId, data: "Session connected"), to: -// connection) -// } -// -// case .command: -// // Execute command in terminal session -// guard let sessionId = connection.sessionId, -// let command = message.data else { -// await sendError("Session ID and command required", to: connection) -// return -// } -// -// do { -// let (output, error) = try await terminalManager.executeCommand(sessionId: sessionId, command: command) -// -// if !output.isEmpty { -// await sendMessage(WSMessage(type: .output, sessionId: sessionId.uuidString, data: output), to: -// connection) -// } -// -// if !error.isEmpty { -// await sendMessage(WSMessage(type: .error, sessionId: sessionId.uuidString, data: error), to: -// connection) -// } -// } catch { -// await sendError(error.localizedDescription, to: connection) -// } -// -// case .ping: -// // Respond to ping with pong -// await sendMessage(WSMessage(type: .pong), to: connection) -// -// case .close: -// // Close the session -// if let sessionId = connection.sessionId { -// await terminalManager.closeSession(id: sessionId) -// } -// try? await connection.websocket.close() -// -// default: -// logger.warning("Unhandled message type: \(message.type)") -// } -// } -// -// private func handleClose(connection: Connection) async { -// logger.info("WebSocket connection closed: \(connection.id)") -// -// await MainActor.run { -// activeConnections.removeValue(forKey: connection.id) -// } -// -// // Clean up associated session if any -// if let sessionId = connection.sessionId { -// await terminalManager.closeSession(id: sessionId) -// } -// -// connection.isClosed = true -// } -// -// private func sendMessage(_ message: WSMessage, to connection: Connection) async { -// do { -// let data = try JSONEncoder().encode(message) -// let text = String(data: data, encoding: .utf8) ?? "{}" -// try await connection.websocket.send(text: text) -// } catch { -// logger.error("Failed to send WebSocket message: \(error)") -// } -// } -// -// private func sendError(_ error: String, to connection: Connection) async { -// await sendMessage(WSMessage(type: .error, data: error), to: connection) -// } -// -// /// WebSocket connection wrapper -// class Connection { -// let id: UUID -// let websocket: WebSocket -// var sessionId: UUID? -// var isClosed = false -// -// init(id: UUID, websocket: WebSocket) { -// self.id = id -// self.websocket = websocket -// } -// } -// } -// -// /// Extension to add WebSocket routes to the router -// extension RouterBuilder { -// mutating func addWebSocketRoutes(terminalManager: TerminalManager) { -// let wsHandler = WebSocketHandler(terminalManager: terminalManager) -// -// // WebSocket endpoint for terminal streaming -// ws("/ws/terminal") { request, ws, context in -// await wsHandler.handle(ws: ws, context: context) -// } -// } -// }