Skip to content

Commit c8ad579

Browse files
committed
feat: support user-supplied literal headers
1 parent 5d97953 commit c8ad579

17 files changed

+472
-64
lines changed

Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

+139-35
Large diffs are not rendered by default.

Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"originHash" : "ec40e522ec1a2416e8e8f5cbe97424ab3e4a614e6ef453c10ea28e84e88b6771",
2+
"originHash" : "b52ef58779afac669f0b78fbf402855ebb45d016ab69ee39b5470c9442c12823",
33
"pins" : [
44
{
55
"identity" : "fluid-menu-bar-extra",
@@ -18,6 +18,15 @@
1818
"revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf"
1919
}
2020
},
21+
{
22+
"identity" : "launchatlogin-modern",
23+
"kind" : "remoteSourceControl",
24+
"location" : "https://github.com/sindresorhus/LaunchAtLogin-modern",
25+
"state" : {
26+
"revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc",
27+
"version" : "1.1.0"
28+
}
29+
},
2130
{
2231
"identity" : "mocker",
2332
"kind" : "remoteSourceControl",
@@ -27,6 +36,15 @@
2736
"version" : "3.0.2"
2837
}
2938
},
39+
{
40+
"identity" : "settingsaccess",
41+
"kind" : "remoteSourceControl",
42+
"location" : "https://github.com/orchetect/SettingsAccess",
43+
"state" : {
44+
"revision" : "08e80c35501f273afa2f5d6f737429bbe395ff81",
45+
"version" : "2.1.0"
46+
}
47+
},
3048
{
3149
"identity" : "swift-protobuf",
3250
"kind" : "remoteSourceControl",
@@ -41,8 +59,8 @@
4159
"kind" : "remoteSourceControl",
4260
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
4361
"state" : {
44-
"revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853",
45-
"version" : "0.57.1"
62+
"revision" : "fac0c3d3ac69b15ea5382275dbbd5e583a2e05fa",
63+
"version" : "0.58.0"
4664
}
4765
},
4866
{

Coder Desktop/Coder Desktop/About.swift

+1-9
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,7 @@ enum About {
3232

3333
@MainActor
3434
static func open() {
35-
#if compiler(>=5.9) && canImport(AppKit)
36-
if #available(macOS 14, *) {
37-
NSApp.activate()
38-
} else {
39-
NSApp.activate(ignoringOtherApps: true)
40-
}
41-
#else
42-
NSApp.activate(ignoringOtherApps: true)
43-
#endif
35+
appActivate()
4436
NSApp.orderFrontStandardAboutPanel(options: [
4537
.credits: credits,
4638
])

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FluidMenuBarExtra
2+
import SwiftData
23
import SwiftUI
34

45
@main
@@ -14,6 +15,11 @@ struct DesktopApp: App {
1415
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
1516
}
1617
.windowResizability(.contentSize)
18+
SwiftUI.Settings { SettingsView<PreviewVPN>()
19+
.environmentObject(appDelegate.vpn)
20+
.environmentObject(appDelegate.settings)
21+
}
22+
.windowResizability(.contentSize)
1723
}
1824
}
1925

@@ -22,10 +28,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2228
private var menuBarExtra: FluidMenuBarExtra?
2329
let vpn: PreviewVPN
2430
let session: PreviewSession
31+
let settings: Settings
2532

2633
override init() {
27-
// TODO: Replace with real implementations
34+
// TODO: Replace with real implementation
2835
vpn = PreviewVPN()
36+
settings = Settings()
2937
session = PreviewSession()
3038
}
3139

@@ -34,10 +42,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3442
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
3543
.environmentObject(self.vpn)
3644
.environmentObject(self.session)
45+
.environmentObject(self.settings)
3746
}
3847
}
3948

4049
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
4150
false
4251
}
4352
}
53+
54+
@MainActor
55+
func appActivate() {
56+
#if compiler(>=5.9) && canImport(AppKit)
57+
if #available(macOS 14, *) {
58+
NSApp.activate()
59+
} else {
60+
NSApp.activate(ignoringOtherApps: true)
61+
}
62+
#else
63+
NSApp.activate(ignoringOtherApps: true)
64+
#endif
65+
}

Coder Desktop/Coder Desktop/Session.swift Coder Desktop/Coder Desktop/State.swift

+46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import CoderSDK
12
import Foundation
23
import KeychainAccess
34
import NetworkExtension
5+
import SwiftUI
46

57
protocol Session: ObservableObject {
68
var hasSession: Bool { get }
@@ -89,3 +91,47 @@ class SecureSession: ObservableObject, Session {
8991
static let sessionToken = "sessionToken"
9092
}
9193
}
94+
95+
class Settings: ObservableObject {
96+
let store: UserDefaults
97+
@AppStorage(Keys.useLiteralHeaders) var useLiteralHeaders = false
98+
99+
@Published var literalHeaders: [LiteralHeader] {
100+
didSet {
101+
try? store.set(JSONEncoder().encode(literalHeaders), forKey: Keys.literalHeaders)
102+
}
103+
}
104+
105+
init(store: UserDefaults = UserDefaults.standard) {
106+
self.store = store
107+
_literalHeaders = Published(
108+
initialValue: UserDefaults.standard.data(
109+
forKey: Keys.literalHeaders
110+
).flatMap { try? JSONDecoder().decode([LiteralHeader].self, from: $0) } ?? []
111+
)
112+
}
113+
114+
enum Keys {
115+
static let useLiteralHeaders = "UseLiteralHeaders"
116+
static let literalHeaders = "LiteralHeaders"
117+
}
118+
}
119+
120+
struct LiteralHeader: Hashable, Identifiable, Equatable, Codable {
121+
var header: String
122+
var value: String
123+
var id: String {
124+
"\(header):\(value)"
125+
}
126+
127+
init(header: String, value: String) {
128+
self.header = header
129+
self.value = value
130+
}
131+
}
132+
133+
extension LiteralHeader {
134+
func toSDKHeader() -> HTTPHeader {
135+
return .init(header: header, value: value)
136+
}
137+
}

Coder Desktop/Coder Desktop/Views/LoginForm.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import CoderSDK
2+
import SwiftData
23
import SwiftUI
34

45
struct LoginForm<S: Session>: View {
56
@EnvironmentObject var session: S
7+
@EnvironmentObject var settings: Settings
68
@Environment(\.dismiss) private var dismiss
79

810
@State private var baseAccessURL: String = ""
@@ -68,7 +70,7 @@ struct LoginForm<S: Session>: View {
6870
}
6971
loading = true
7072
defer { loading = false }
71-
let client = Client(url: url, token: sessionToken)
73+
let client = Client(url: url, token: sessionToken, headers: settings.literalHeaders.map { $0.toSDKHeader() })
7274
do {
7375
_ = try await client.user("me")
7476
} catch {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import LaunchAtLogin
2+
import SwiftUI
3+
4+
struct GeneralTab: View {
5+
var body: some View {
6+
Form {
7+
Section {
8+
LaunchAtLogin.Toggle("Launch at Login")
9+
}
10+
}.formStyle(.grouped)
11+
}
12+
}
13+
14+
#Preview {
15+
GeneralTab()
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct LiteralHeaderModal: View {
5+
var existingHeader: LiteralHeader?
6+
7+
@EnvironmentObject var settings: Settings
8+
@Environment(\.dismiss) private var dismiss
9+
10+
@State private var header: String = ""
11+
@State private var value: String = ""
12+
13+
var body: some View {
14+
VStack(spacing: 0) {
15+
Form {
16+
Section {
17+
TextField("Header", text: $header)
18+
TextField("Value", text: $value)
19+
}
20+
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
21+
Divider()
22+
HStack {
23+
Spacer()
24+
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
25+
Button(existingHeader == nil ? "Add" : "Save", action: submit)
26+
.keyboardShortcut(.defaultAction)
27+
}.padding(20)
28+
}.onAppear {
29+
if let existingHeader {
30+
self.header = existingHeader.header
31+
self.value = existingHeader.value
32+
}
33+
}
34+
}
35+
36+
func submit() {
37+
defer { dismiss() }
38+
if let existingHeader {
39+
settings.literalHeaders.removeAll { $0 == existingHeader }
40+
}
41+
let newHeader = LiteralHeader(header: header, value: value)
42+
if !settings.literalHeaders.contains(newHeader) {
43+
settings.literalHeaders.append(newHeader)
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct LiteralHeadersSection<VPN: VPNService>: View {
5+
@EnvironmentObject var vpn: VPN
6+
@EnvironmentObject var settings: Settings
7+
8+
@State private var selectedHeader: LiteralHeader.ID?
9+
@State private var editingHeader: LiteralHeader?
10+
@State private var addingNewHeader = false
11+
12+
let inspection = Inspection<Self>()
13+
14+
var body: some View {
15+
Section {
16+
Toggle(isOn: settings.$useLiteralHeaders) {
17+
Text("HTTP Headers")
18+
Text("When enabled, these headers will be included on all outgoing HTTP requests.")
19+
if vpn.state != .disabled { Text("Cannot be modified while Coder VPN is enabled.") }
20+
}
21+
.controlSize(.large)
22+
23+
Table(settings.literalHeaders, selection: $selectedHeader) {
24+
TableColumn("Header", value: \.header)
25+
TableColumn("Value", value: \.value)
26+
}.opacity(settings.useLiteralHeaders ? 1 : 0.5)
27+
.frame(minWidth: 400, minHeight: 200)
28+
.padding(.bottom, 25)
29+
.overlay(alignment: .bottom) {
30+
VStack(alignment: .leading, spacing: 0) {
31+
Divider()
32+
HStack(spacing: 0) {
33+
Button {
34+
addingNewHeader = true
35+
} label: {
36+
Image(systemName: "plus")
37+
.frame(width: 24, height: 24)
38+
}
39+
Divider()
40+
Button {
41+
settings.literalHeaders.removeAll { $0.id == selectedHeader }
42+
selectedHeader = nil
43+
} label: {
44+
Image(systemName: "minus")
45+
.frame(width: 24, height: 24)
46+
}.disabled(selectedHeader == nil)
47+
}
48+
.buttonStyle(.borderless)
49+
}
50+
.background(.primary.opacity(0.04))
51+
.fixedSize(horizontal: false, vertical: true)
52+
}
53+
.background(.primary.opacity(0.04))
54+
.contextMenu(forSelectionType: LiteralHeader.ID.self, menu: { _ in },
55+
primaryAction: { selectedHeaders in
56+
if let firstHeader = selectedHeaders.first {
57+
editingHeader = settings.literalHeaders.first(where: { $0.id == firstHeader })
58+
}
59+
})
60+
.disabled(!settings.useLiteralHeaders)
61+
}
62+
.sheet(isPresented: $addingNewHeader) {
63+
LiteralHeaderModal()
64+
}
65+
.sheet(item: $editingHeader) { header in
66+
LiteralHeaderModal(existingHeader: header)
67+
}.onTapGesture {
68+
selectedHeader = nil
69+
}.disabled(vpn.state != .disabled)
70+
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import SwiftData
2+
import SwiftUI
3+
4+
struct NetworkTab<VPN: VPNService>: View {
5+
var body: some View {
6+
Form {
7+
LiteralHeadersSection<VPN>()
8+
}
9+
.formStyle(.grouped)
10+
}
11+
}
12+
13+
#Preview {
14+
NetworkTab<PreviewVPN>()
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import SwiftUI
2+
3+
struct SettingsView<VPN: VPNService>: View {
4+
@AppStorage("SettingsSelectedIndex") private var selection: SettingsTab = .general
5+
6+
var body: some View {
7+
TabView(selection: $selection) {
8+
GeneralTab()
9+
.tabItem {
10+
Label("General", systemImage: "gearshape")
11+
}.tag(SettingsTab.general)
12+
NetworkTab<VPN>()
13+
.tabItem {
14+
Label("Network", systemImage: "dot.radiowaves.left.and.right")
15+
}.tag(SettingsTab.network)
16+
}.frame(width: 600)
17+
.frame(maxHeight: 500)
18+
.scrollContentBackground(.hidden)
19+
.fixedSize()
20+
}
21+
}
22+
23+
enum SettingsTab: Int {
24+
case general
25+
case network
26+
}

Coder Desktop/Coder Desktop/Views/Util.swift

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Combine
2+
import SwiftUI
23

34
// This is required for inspecting stateful views
45
final class Inspection<V> {

0 commit comments

Comments
 (0)