diff --git a/VibeTunnel.xcodeproj/project.pbxproj b/VibeTunnel.xcodeproj/project.pbxproj index 66ac8b0c..09214eaa 100644 --- a/VibeTunnel.xcodeproj/project.pbxproj +++ b/VibeTunnel.xcodeproj/project.pbxproj @@ -125,6 +125,7 @@ 788687ED2DFF4FCB00B22C15 /* Sources */, 788687EE2DFF4FCB00B22C15 /* Frameworks */, 788687EF2DFF4FCB00B22C15 /* Resources */, + B2C3D4E5F6A7B8C9D0E1F234 /* Build Web Frontend */, A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */, ); buildRules = ( @@ -262,6 +263,31 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + B2C3D4E5F6A7B8C9D0E1F234 /* Build Web Frontend */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/web/package.json", + "$(SRCROOT)/web/src", + "$(SRCROOT)/web/tsconfig.json", + "$(SRCROOT)/web/tsconfig.client.json", + "$(SRCROOT)/web/tailwind.config.js", + ); + name = "Build Web Frontend"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Resources/web/public", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Build web frontend\necho \"Building web frontend...\"\n\n# Setup PATH to include common Node.js installation locations\nexport PATH=\"/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.nvm/versions/node/$(ls -1 $HOME/.nvm/versions/node 2>/dev/null | tail -1)/bin:$PATH\"\n\n# Get the project directory\nPROJECT_DIR=\"${SRCROOT}\"\nWEB_DIR=\"${PROJECT_DIR}/web\"\nPUBLIC_DIR=\"${WEB_DIR}/public\"\nDEST_DIR=\"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public\"\n\n# Export CI environment variable to prevent interactive prompts\nexport CI=true\n\n# Check if npm is available\nif ! command -v npm &> /dev/null; then\n echo \"error: npm could not be found in PATH\"\n echo \"PATH is: $PATH\"\n echo \"Please ensure Node.js is installed\"\n exit 1\nfi\n\n# Print npm version for debugging\necho \"Using npm version: $(npm --version)\"\necho \"Using node version: $(node --version)\"\n\n# Check if web directory exists\nif [ ! -d \"${WEB_DIR}\" ]; then\n echo \"error: Web directory not found at ${WEB_DIR}\"\n exit 1\nfi\n\n# Change to web directory\ncd \"${WEB_DIR}\"\n\n# Install dependencies if node_modules doesn't exist or package-lock.json is newer\nif [ ! -d \"node_modules\" ] || [ \"package-lock.json\" -nt \"node_modules\" ]; then\n echo \"Installing npm dependencies...\"\n npm install --no-progress --no-audit\n if [ $? -ne 0 ]; then\n echo \"error: npm install failed\"\n exit 1\n fi\nfi\n\n# Build the web frontend\necho \"Running npm build...\"\nnpm run build\nif [ $? -ne 0 ]; then\n echo \"error: npm run build failed\"\n exit 1\nfi\n\n# Create destination directory\nmkdir -p \"${DEST_DIR}\"\n\n# Copy built files to Resources\necho \"Copying web files to app bundle...\"\nif [ -d \"${PUBLIC_DIR}\" ]; then\n # Copy all files from public directory\n cp -R \"${PUBLIC_DIR}/\"* \"${DEST_DIR}/\"\n echo \"Web frontend files copied to ${DEST_DIR}\"\nelse\n echo \"error: Public directory not found at ${PUBLIC_DIR}\"\n exit 1\nfi\n"; + }; A189466CB0AD49BEBE16B954 /* Build tty-fwd Universal Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/VibeTunnel/Core/Services/TunnelServer.swift b/VibeTunnel/Core/Services/TunnelServer.swift index ad165a81..3ae48780 100644 --- a/VibeTunnel/Core/Services/TunnelServer.swift +++ b/VibeTunnel/Core/Services/TunnelServer.swift @@ -474,54 +474,28 @@ public final class TunnelServer { // MARK: - Static File Serving private func serveStaticFile(path: String) async -> Response { - // Try multiple possible paths for the web/public directory - let possiblePaths = [ - // Bundle resource path (for production app) - Bundle.main.resourcePath?.appending("/web/public"), - // Current working directory (for development) - FileManager.default.currentDirectoryPath + "/web/public", - // Project directory (if running from source) - "/Users/mitsuhiko/Development/vibetunnel/web/public", - // Relative to bundle path - Bundle.main.bundlePath + "/../../../web/public" - ].compactMap(\.self) - + // Serve files only from the bundled Resources folder + guard let resourcePath = Bundle.main.resourcePath else { + logger.error("Bundle resource path not found") + return errorResponse(message: "Resource bundle not available", status: .internalServerError) + } + + let webPublicPath = resourcePath + "/web/public" + + // Sanitize path to prevent directory traversal attacks let sanitizedPath = path.replacingOccurrences(of: "..", with: "") - - var webPublicPath: String? - var fullPath: String? - - // Find the first path that exists - for testPath in possiblePaths { - let testFullPath = testPath + "/" + sanitizedPath - if FileManager.default.fileExists(atPath: testFullPath) { - webPublicPath = testPath - fullPath = testFullPath - break - } - } - - // If no file found, try just checking directory existence - if fullPath == nil { - for testPath in possiblePaths { - if FileManager.default.fileExists(atPath: testPath, isDirectory: nil) { - webPublicPath = testPath - fullPath = testPath + "/" + sanitizedPath - break - } - } - } - - guard let finalPath = fullPath, webPublicPath != nil else { - logger.error("Could not find web/public directory in any of these paths:") - for testPath in possiblePaths { - logger.error(" - \(testPath)") - } - return errorResponse(message: "Web directory not found", status: .notFound) + let fullPath = webPublicPath + "/" + sanitizedPath + + // Check if the web directory exists in Resources + var isWebDirExists: ObjCBool = false + if !FileManager.default.fileExists(atPath: webPublicPath, isDirectory: &isWebDirExists) || !isWebDirExists.boolValue { + logger.error("Web resources not found at: \(webPublicPath)") + logger.error("Make sure the app was built with the 'Build Web Frontend' phase") + return errorResponse(message: "Web resources not bundled", status: .internalServerError) } var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: finalPath, isDirectory: &isDirectory) else { + guard FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDirectory) else { return errorResponse(message: "File not found", status: .notFound) } @@ -531,7 +505,7 @@ public final class TunnelServer { } do { - let fileData = try Data(contentsOf: URL(fileURLWithPath: finalPath)) + let fileData = try Data(contentsOf: URL(fileURLWithPath: fullPath)) var buffer = ByteBuffer() buffer.writeBytes(fileData)