Skip to content

Commit 85e0300

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

17 files changed

+467
-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

+22-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ struct DesktopApp: App {
1414
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
1515
}
1616
.windowResizability(.contentSize)
17+
SwiftUI.Settings { SettingsView<PreviewVPN>()
18+
.environmentObject(appDelegate.vpn)
19+
.environmentObject(appDelegate.settings)
20+
}
21+
.windowResizability(.contentSize)
1722
}
1823
}
1924

@@ -22,10 +27,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2227
private var menuBarExtra: FluidMenuBarExtra?
2328
let vpn: PreviewVPN
2429
let session: PreviewSession
30+
let settings: Settings
2531

2632
override init() {
27-
// TODO: Replace with real implementations
33+
// TODO: Replace with real implementation
2834
vpn = PreviewVPN()
35+
settings = Settings()
2936
session = PreviewSession()
3037
}
3138

@@ -34,10 +41,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3441
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
3542
.environmentObject(self.vpn)
3643
.environmentObject(self.session)
44+
.environmentObject(self.settings)
3745
}
3846
}
3947

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

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33

44
struct LoginForm<S: Session>: View {
55
@EnvironmentObject var session: S
6+
@EnvironmentObject var settings: Settings
67
@Environment(\.dismiss) private var dismiss
78

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