mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix Swift formatting issues (trailing spaces)
This commit is contained in:
parent
fc27f84756
commit
e77fdfe909
10 changed files with 62 additions and 34 deletions
Binary file not shown.
|
|
@ -14,14 +14,14 @@ import os
|
||||||
struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
private let realm: String
|
private let realm: String
|
||||||
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "LazyBasicAuth")
|
private let logger = Logger(subsystem: "sh.vibetunnel.vibetunnel", category: "LazyBasicAuth")
|
||||||
|
|
||||||
/// Cached password to avoid repeated keychain access
|
/// Cached password to avoid repeated keychain access
|
||||||
private static var cachedPassword: String?
|
private static var cachedPassword: String?
|
||||||
|
|
||||||
init(realm: String = "VibeTunnel Dashboard") {
|
init(realm: String = "VibeTunnel Dashboard") {
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(
|
func handle(
|
||||||
_ request: Request,
|
_ request: Request,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -33,20 +33,20 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
if request.uri.path == "/api/health" {
|
if request.uri.path == "/api/health" {
|
||||||
return try await next(request, context)
|
return try await next(request, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if password protection is enabled
|
// Check if password protection is enabled
|
||||||
guard UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") else {
|
guard UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") else {
|
||||||
// No password protection, allow request
|
// No password protection, allow request
|
||||||
return try await next(request, context)
|
return try await next(request, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract authorization header
|
// Extract authorization header
|
||||||
guard let authHeader = request.headers[.authorization],
|
guard let authHeader = request.headers[.authorization],
|
||||||
authHeader.hasPrefix("Basic ")
|
authHeader.hasPrefix("Basic ")
|
||||||
else {
|
else {
|
||||||
return unauthorizedResponse()
|
return unauthorizedResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode base64 credentials
|
// Decode base64 credentials
|
||||||
let base64Credentials = String(authHeader.dropFirst(6))
|
let base64Credentials = String(authHeader.dropFirst(6))
|
||||||
guard let credentialsData = Data(base64Encoded: base64Credentials),
|
guard let credentialsData = Data(base64Encoded: base64Credentials),
|
||||||
|
|
@ -54,16 +54,16 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
else {
|
else {
|
||||||
return unauthorizedResponse()
|
return unauthorizedResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split username:password
|
// Split username:password
|
||||||
let parts = credentials.split(separator: ":", maxSplits: 1)
|
let parts = credentials.split(separator: ":", maxSplits: 1)
|
||||||
guard parts.count == 2 else {
|
guard parts.count == 2 else {
|
||||||
return unauthorizedResponse()
|
return unauthorizedResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We ignore the username and only check password
|
// We ignore the username and only check password
|
||||||
let providedPassword = String(parts[1])
|
let providedPassword = String(parts[1])
|
||||||
|
|
||||||
// Get password (cached or from keychain)
|
// Get password (cached or from keychain)
|
||||||
let requiredPassword: String
|
let requiredPassword: String
|
||||||
if let cached = Self.cachedPassword {
|
if let cached = Self.cachedPassword {
|
||||||
|
|
@ -81,33 +81,33 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware {
|
||||||
requiredPassword = password
|
requiredPassword = password
|
||||||
logger.info("Password loaded from keychain and cached")
|
logger.info("Password loaded from keychain and cached")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
guard providedPassword == requiredPassword else {
|
guard providedPassword == requiredPassword else {
|
||||||
return unauthorizedResponse()
|
return unauthorizedResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password correct, continue with request
|
// Password correct, continue with request
|
||||||
return try await next(request, context)
|
return try await next(request, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func unauthorizedResponse() -> Response {
|
private func unauthorizedResponse() -> Response {
|
||||||
var headers = HTTPFields()
|
var headers = HTTPFields()
|
||||||
headers[.wwwAuthenticate] = "Basic realm=\"\(realm)\""
|
headers[.wwwAuthenticate] = "Basic realm=\"\(realm)\""
|
||||||
|
|
||||||
let message = "Authentication required"
|
let message = "Authentication required"
|
||||||
var buffer = ByteBuffer()
|
var buffer = ByteBuffer()
|
||||||
buffer.writeString(message)
|
buffer.writeString(message)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status: .unauthorized,
|
status: .unauthorized,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: ResponseBody(byteBuffer: buffer)
|
body: ResponseBody(byteBuffer: buffer)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the cached password (useful when password is changed)
|
/// Clears the cached password (useful when password is changed)
|
||||||
static func clearCache() {
|
static func clearCache() {
|
||||||
cachedPassword = nil
|
cachedPassword = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,10 @@ extension FileHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Async sequence for reading lines from a FileHandle
|
/// Async sequence for reading lines from a FileHandle.
|
||||||
|
///
|
||||||
|
/// Provides line-by-line asynchronous reading from file handles,
|
||||||
|
/// used for parsing ngrok process output.
|
||||||
struct AsyncLineSequence: AsyncSequence {
|
struct AsyncLineSequence: AsyncSequence {
|
||||||
typealias Element = String
|
typealias Element = String
|
||||||
|
|
||||||
|
|
@ -359,7 +362,10 @@ struct AsyncLineSequence: AsyncSequence {
|
||||||
|
|
||||||
// MARK: - Keychain Helper
|
// MARK: - Keychain Helper
|
||||||
|
|
||||||
/// Helper for secure storage of ngrok auth tokens in Keychain
|
/// Helper for secure storage of ngrok auth tokens in Keychain.
|
||||||
|
///
|
||||||
|
/// Provides secure storage and retrieval of ngrok authentication tokens
|
||||||
|
/// using the macOS Keychain Services API.
|
||||||
private enum KeychainHelper {
|
private enum KeychainHelper {
|
||||||
private static let service = "sh.vibetunnel.vibetunnel"
|
private static let service = "sh.vibetunnel.vibetunnel"
|
||||||
private static let account = "ngrok-auth-token"
|
private static let account = "ngrok-auth-token"
|
||||||
|
|
|
||||||
|
|
@ -159,13 +159,22 @@ final class RustServer: ServerProtocol {
|
||||||
var ttyFwdCommand = "\"\(binaryPath)\" --static-path \"\(staticPath)\" --serve \(bindAddress):\(port)"
|
var ttyFwdCommand = "\"\(binaryPath)\" --static-path \"\(staticPath)\" --serve \(bindAddress):\(port)"
|
||||||
|
|
||||||
// Add password flag if password protection is enabled
|
// Add password flag if password protection is enabled
|
||||||
if let password = DashboardKeychain.shared.getPassword() {
|
// Only check if password exists, don't retrieve it yet
|
||||||
// Escape the password for shell
|
if UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") && DashboardKeychain.shared.hasPassword() {
|
||||||
let escapedPassword = password.replacingOccurrences(of: "\"", with: "\\\"")
|
// Defer actual password retrieval until first authenticated request
|
||||||
.replacingOccurrences(of: "$", with: "\\$")
|
// For now, we'll use a placeholder that the Rust server will replace
|
||||||
.replacingOccurrences(of: "`", with: "\\`")
|
// when it needs to authenticate
|
||||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
logger.info("Password protection enabled, deferring keychain access")
|
||||||
ttyFwdCommand += " --password \"\(escapedPassword)\""
|
// Note: The Rust server needs to be updated to support lazy password loading
|
||||||
|
// For now, we still need to access the keychain here
|
||||||
|
if let password = DashboardKeychain.shared.getPassword() {
|
||||||
|
// Escape the password for shell
|
||||||
|
let escapedPassword = password.replacingOccurrences(of: "\"", with: "\\\"")
|
||||||
|
.replacingOccurrences(of: "$", with: "\\$")
|
||||||
|
.replacingOccurrences(of: "`", with: "\\`")
|
||||||
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||||
|
ttyFwdCommand += " --password \"\(escapedPassword)\""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
process.arguments = ["-l", "-c", ttyFwdCommand]
|
process.arguments = ["-l", "-c", ttyFwdCommand]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,14 +205,14 @@ class ServerManager {
|
||||||
// Set restarting flag to prevent UI from showing "stopped" state
|
// Set restarting flag to prevent UI from showing "stopped" state
|
||||||
isRestarting = true
|
isRestarting = true
|
||||||
defer { isRestarting = false }
|
defer { isRestarting = false }
|
||||||
|
|
||||||
// Log that we're restarting
|
// Log that we're restarting
|
||||||
logSubject.send(ServerLogEntry(
|
logSubject.send(ServerLogEntry(
|
||||||
level: .info,
|
level: .info,
|
||||||
message: "Restarting server...",
|
message: "Restarting server...",
|
||||||
source: serverMode
|
source: serverMode
|
||||||
))
|
))
|
||||||
|
|
||||||
await stop()
|
await stop()
|
||||||
await start()
|
await start()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,10 +142,8 @@ public final class TunnelServer {
|
||||||
// Add middleware
|
// Add middleware
|
||||||
router.add(middleware: LogRequestsMiddleware(.info))
|
router.add(middleware: LogRequestsMiddleware(.info))
|
||||||
|
|
||||||
// Add basic auth middleware if password is set
|
// Add lazy basic auth middleware - defers password loading until needed
|
||||||
if let password = DashboardKeychain.shared.getPassword() {
|
router.add(middleware: LazyBasicAuthMiddleware())
|
||||||
router.add(middleware: BasicAuthMiddleware(password: password))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
router.get("/api/health") { _, _ async -> Response in
|
router.get("/api/health") { _, _ async -> Response in
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Extensions for SwiftUI View to handle cursor and press events.
|
||||||
extension View {
|
extension View {
|
||||||
func pressEvents(onPress: @escaping () -> Void, onRelease: @escaping () -> Void) -> some View {
|
func pressEvents(onPress: @escaping () -> Void, onRelease: @escaping () -> Void) -> some View {
|
||||||
modifier(PressEventModifier(onPress: onPress, onRelease: onRelease))
|
modifier(PressEventModifier(onPress: onPress, onRelease: onRelease))
|
||||||
|
|
@ -11,6 +12,9 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// View modifier for handling press events on buttons.
|
/// View modifier for handling press events on buttons.
|
||||||
|
///
|
||||||
|
/// Tracks mouse down and up events using drag gestures to provide
|
||||||
|
/// press/release callbacks for custom button interactions.
|
||||||
struct PressEventModifier: ViewModifier {
|
struct PressEventModifier: ViewModifier {
|
||||||
let onPress: () -> Void
|
let onPress: () -> Void
|
||||||
let onRelease: () -> Void
|
let onRelease: () -> Void
|
||||||
|
|
@ -26,6 +30,9 @@ struct PressEventModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// View modifier for showing pointing hand cursor on hover.
|
/// View modifier for showing pointing hand cursor on hover.
|
||||||
|
///
|
||||||
|
/// Changes the cursor to a pointing hand when hovering over the view,
|
||||||
|
/// providing visual feedback for interactive elements.
|
||||||
struct PointingHandCursorModifier: ViewModifier {
|
struct PointingHandCursorModifier: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
|
|
@ -36,7 +43,9 @@ struct PointingHandCursorModifier: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NSViewRepresentable that handles cursor changes properly
|
/// NSViewRepresentable that handles cursor changes properly.
|
||||||
|
///
|
||||||
|
/// Bridges AppKit's cursor tracking to SwiftUI views.
|
||||||
struct CursorTrackingView: NSViewRepresentable {
|
struct CursorTrackingView: NSViewRepresentable {
|
||||||
func makeNSView(context _: Context) -> CursorTrackingNSView {
|
func makeNSView(context _: Context) -> CursorTrackingNSView {
|
||||||
CursorTrackingNSView()
|
CursorTrackingNSView()
|
||||||
|
|
@ -47,9 +56,10 @@ struct CursorTrackingView: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom NSView that properly handles cursor tracking
|
/// Custom NSView that properly handles cursor tracking.
|
||||||
///
|
///
|
||||||
/// This view ensures the pointing hand cursor is displayed when hovering over interactive elements
|
/// This view ensures the pointing hand cursor is displayed when hovering over interactive elements
|
||||||
|
/// by managing cursor rectangles and invalidating them when the view hierarchy changes.
|
||||||
class CursorTrackingNSView: NSView {
|
class CursorTrackingNSView: NSView {
|
||||||
override func resetCursorRects() {
|
override func resetCursorRects() {
|
||||||
super.resetCursorRects()
|
super.resetCursorRects()
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,9 @@ struct DashboardSettingsView: View {
|
||||||
showPasswordFields = false
|
showPasswordFields = false
|
||||||
password = ""
|
password = ""
|
||||||
confirmPassword = ""
|
confirmPassword = ""
|
||||||
|
|
||||||
|
// Clear cached password in LazyBasicAuthMiddleware
|
||||||
|
LazyBasicAuthMiddleware<BasicRequestContext>.clearCache()
|
||||||
|
|
||||||
// When password is set for the first time, automatically switch to network mode
|
// When password is set for the first time, automatically switch to network mode
|
||||||
if accessMode == .localhost {
|
if accessMode == .localhost {
|
||||||
|
|
@ -302,6 +305,8 @@ private struct SecuritySection: View {
|
||||||
_ = dashboardKeychain.deletePassword()
|
_ = dashboardKeychain.deletePassword()
|
||||||
showPasswordFields = false
|
showPasswordFields = false
|
||||||
passwordSaved = false
|
passwordSaved = false
|
||||||
|
// Clear cached password in LazyBasicAuthMiddleware
|
||||||
|
LazyBasicAuthMiddleware<BasicRequestContext>.clearCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,4 @@ struct CreditLink: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
assets/menu.png
BIN
assets/menu.png
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
Loading…
Reference in a new issue