Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support user-supplied literal headers #24

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 122 additions & 35 deletions Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "ec40e522ec1a2416e8e8f5cbe97424ab3e4a614e6ef453c10ea28e84e88b6771",
"originHash" : "c41f63aa01c78f450e2232efbefcd30874995ad120db77fa5942062d6f813891",
"pins" : [
{
"identity" : "fluid-menu-bar-extra",
Expand All @@ -18,6 +18,15 @@
"revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf"
}
},
{
"identity" : "launchatlogin-modern",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/LaunchAtLogin-modern",
"state" : {
"revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc",
"version" : "1.1.0"
}
},
{
"identity" : "mocker",
"kind" : "remoteSourceControl",
Expand All @@ -41,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
"state" : {
"revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853",
"version" : "0.57.1"
"revision" : "fac0c3d3ac69b15ea5382275dbbd5e583a2e05fa",
"version" : "0.58.0"
}
},
{
Expand Down
10 changes: 1 addition & 9 deletions Coder Desktop/Coder Desktop/About.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,7 @@ enum About {

@MainActor
static func open() {
#if compiler(>=5.9) && canImport(AppKit)
if #available(macOS 14, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
appActivate()
NSApp.orderFrontStandardAboutPanel(options: [
.credits: credits,
])
Expand Down
15 changes: 14 additions & 1 deletion Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ struct DesktopApp: App {
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
}
.windowResizability(.contentSize)
SwiftUI.Settings { SettingsView<PreviewVPN>()
.environmentObject(appDelegate.vpn)
.environmentObject(appDelegate.settings)
}
.windowResizability(.contentSize)
}
}

Expand All @@ -22,10 +27,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBarExtra: FluidMenuBarExtra?
let vpn: PreviewVPN
let session: PreviewSession
let settings: Settings

override init() {
// TODO: Replace with real implementations
// TODO: Replace with real implementation
vpn = PreviewVPN()
settings = Settings()
session = PreviewSession()
}

Expand All @@ -34,6 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
.environmentObject(self.vpn)
.environmentObject(self.session)
.environmentObject(self.settings)
}
}

Expand All @@ -49,3 +57,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
false
}
}

@MainActor
func appActivate() {
NSApp.activate()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import CoderSDK
import Foundation
import KeychainAccess
import NetworkExtension
import SwiftUI

protocol Session: ObservableObject {
var hasSession: Bool { get }
Expand Down Expand Up @@ -89,3 +91,47 @@ class SecureSession: ObservableObject, Session {
static let sessionToken = "sessionToken"
}
}

class Settings: ObservableObject {
private let store: UserDefaults
@AppStorage(Keys.useLiteralHeaders) var useLiteralHeaders = false

@Published var literalHeaders: [LiteralHeader] {
didSet {
try? store.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders)
}
}

init(store: UserDefaults = .standard) {
self.store = store
_literalHeaders = Published(
initialValue: store.data(
forKey: Keys.literalHeaders
).flatMap { try? JSONDecoder().decode([LiteralHeader].self, from: $0) } ?? []
)
}

enum Keys {
static let useLiteralHeaders = "UseLiteralHeaders"
static let literalHeaders = "LiteralHeaders"
}
}

struct LiteralHeader: Hashable, Identifiable, Equatable, Codable {
var header: String
var value: String
var id: String {
"\(header):\(value)"
}

init(header: String, value: String) {
self.header = header
self.value = value
}
}

extension LiteralHeader {
func toSDKHeader() -> HTTPHeader {
return .init(header: header, value: value)
}
}
3 changes: 2 additions & 1 deletion Coder Desktop/Coder Desktop/Views/LoginForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI

struct LoginForm<S: Session>: View {
@EnvironmentObject var session: S
@EnvironmentObject var settings: Settings
@Environment(\.dismiss) private var dismiss

@State private var baseAccessURL: String = ""
Expand Down Expand Up @@ -68,7 +69,7 @@ struct LoginForm<S: Session>: View {
}
loading = true
defer { loading = false }
let client = Client(url: url, token: sessionToken)
let client = Client(url: url, token: sessionToken, headers: settings.literalHeaders.map { $0.toSDKHeader() })
do {
_ = try await client.user("me")
} catch {
Expand Down
16 changes: 16 additions & 0 deletions Coder Desktop/Coder Desktop/Views/Settings/GeneralTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import LaunchAtLogin
import SwiftUI

struct GeneralTab: View {
var body: some View {
Form {
Section {
LaunchAtLogin.Toggle("Launch at Login")
}
}.formStyle(.grouped)
}
}

#Preview {
GeneralTab()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import SwiftUI

struct LiteralHeaderModal: View {
var existingHeader: LiteralHeader?

@EnvironmentObject var settings: Settings
@Environment(\.dismiss) private var dismiss

@State private var header: String = ""
@State private var value: String = ""

var body: some View {
VStack(spacing: 0) {
Form {
Section {
TextField("Header", text: $header)
TextField("Value", text: $value)
}
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
Divider()
HStack {
Spacer()
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
Button(existingHeader == nil ? "Add" : "Save", action: submit)
.keyboardShortcut(.defaultAction)
}.padding(20)
}.onAppear {
if let existingHeader {
self.header = existingHeader.header
self.value = existingHeader.value
}
}
}

func submit() {
defer { dismiss() }
if let existingHeader {
settings.literalHeaders.removeAll { $0 == existingHeader }
}
let newHeader = LiteralHeader(header: header, value: value)
if !settings.literalHeaders.contains(newHeader) {
settings.literalHeaders.append(newHeader)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import SwiftUI

struct LiteralHeadersSection<VPN: VPNService>: View {
@EnvironmentObject var vpn: VPN
@EnvironmentObject var settings: Settings

@State private var selectedHeader: LiteralHeader.ID?
@State private var editingHeader: LiteralHeader?
@State private var addingNewHeader = false

let inspection = Inspection<Self>()

var body: some View {
Section {
Toggle(isOn: settings.$useLiteralHeaders) {
Text("HTTP Headers")
Text("When enabled, these headers will be included on all outgoing HTTP requests.")
if vpn.state != .disabled { Text("Cannot be modified while Coder VPN is enabled.") }
}
.controlSize(.large)

Table(settings.literalHeaders, selection: $selectedHeader) {
TableColumn("Header", value: \.header)
TableColumn("Value", value: \.value)
}.opacity(settings.useLiteralHeaders ? 1 : 0.5)
.frame(minWidth: 400, minHeight: 200)
.padding(.bottom, 25)
.overlay(alignment: .bottom) {
VStack(alignment: .leading, spacing: 0) {
Divider()
HStack(spacing: 0) {
Button {
addingNewHeader = true
} label: {
Image(systemName: "plus")
.frame(width: 24, height: 24)
}
Divider()
Button {
settings.literalHeaders.removeAll { $0.id == selectedHeader }
selectedHeader = nil
} label: {
Image(systemName: "minus")
.frame(width: 24, height: 24)
}.disabled(selectedHeader == nil)
}
.buttonStyle(.borderless)
}
.background(.primary.opacity(0.04))
.fixedSize(horizontal: false, vertical: true)
}
.background(.primary.opacity(0.04))
.contextMenu(forSelectionType: LiteralHeader.ID.self, menu: { _ in },
primaryAction: { selectedHeaders in
if let firstHeader = selectedHeaders.first {
editingHeader = settings.literalHeaders.first(where: { $0.id == firstHeader })
}
})
.disabled(!settings.useLiteralHeaders)
}
.sheet(isPresented: $addingNewHeader) {
LiteralHeaderModal()
}
.sheet(item: $editingHeader) { header in
LiteralHeaderModal(existingHeader: header)
}.onTapGesture {
selectedHeader = nil
}.disabled(vpn.state != .disabled)
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
}
}
14 changes: 14 additions & 0 deletions Coder Desktop/Coder Desktop/Views/Settings/NetworkTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import SwiftUI

struct NetworkTab<VPN: VPNService>: View {
var body: some View {
Form {
LiteralHeadersSection<VPN>()
}
.formStyle(.grouped)
}
}

#Preview {
NetworkTab<PreviewVPN>()
}
26 changes: 26 additions & 0 deletions Coder Desktop/Coder Desktop/Views/Settings/Settings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

struct SettingsView<VPN: VPNService>: View {
@AppStorage("SettingsSelectedIndex") private var selection: SettingsTab = .general

var body: some View {
TabView(selection: $selection) {
GeneralTab()
.tabItem {
Label("General", systemImage: "gearshape")
}.tag(SettingsTab.general)
NetworkTab<VPN>()
.tabItem {
Label("Network", systemImage: "dot.radiowaves.left.and.right")
}.tag(SettingsTab.network)
}.frame(width: 600)
.frame(maxHeight: 500)
.scrollContentBackground(.hidden)
.fixedSize()
}
}

enum SettingsTab: Int {
case general
case network
}
1 change: 1 addition & 0 deletions Coder Desktop/Coder Desktop/Views/Util.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Combine
import SwiftUI

// This is required for inspecting stateful views
final class Inspection<V> {
Expand Down
9 changes: 9 additions & 0 deletions Coder Desktop/Coder Desktop/Views/VPNMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct VPNMenu<VPN: VPNService, S: Session>: View {
@EnvironmentObject var vpn: VPN
@EnvironmentObject var session: S
@Environment(\.openSettings) private var openSettings

let inspection = Inspection<Self>()

Expand All @@ -21,6 +22,8 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
)) {
Text("CoderVPN")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.body.bold())
.foregroundColor(.primary)
}.toggleStyle(.switch)
.disabled(vpnDisabled)
}
Expand Down Expand Up @@ -50,6 +53,12 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
TrayDivider()
}
AuthButton<VPN, S>()
Button {
openSettings()
appActivate()
} label: {
ButtonRowView { Text("Settings") }
}.buttonStyle(.plain)
Button {
About.open()
} label: {
Expand Down
10 changes: 1 addition & 9 deletions Coder Desktop/Coder Desktop/Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,7 @@ enum Windows: String {
extension OpenWindowAction {
// Type-safe wrapper for opening windows that also focuses the new window
func callAsFunction(id: Windows) {
#if compiler(>=5.9) && canImport(AppKit)
if #available(macOS 14, *) {
NSApp.activate()
} else {
NSApp.activate(ignoringOtherApps: true)
}
#else
NSApp.activate(ignoringOtherApps: true)
#endif
appActivate()
callAsFunction(id: id.rawValue)
// The arranging behaviour is flakey without this
NSApp.arrangeInFront(nil)
Expand Down
Loading