Skip to content

Commit fdb8545

Browse files
feat: support user-supplied literal headers (#24)
1 parent 63f29ef commit fdb8545

File tree

17 files changed

+434
-64
lines changed

17 files changed

+434
-64
lines changed

Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

Lines changed: 122 additions & 35 deletions
Large diffs are not rendered by default.

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

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Coder Desktop/Coder Desktop/About.swift

Lines changed: 1 addition & 9 deletions
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

Lines changed: 14 additions & 1 deletion
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,6 +41,7 @@ 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

@@ -49,3 +57,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4957
false
5058
}
5159
}
60+
61+
@MainActor
62+
func appActivate() {
63+
NSApp.activate()
64+
}

Coder Desktop/Coder Desktop/Session.swift renamed to Coder Desktop/Coder Desktop/State.swift

Lines changed: 46 additions & 0 deletions
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+
private 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 = .standard) {
106+
self.store = store
107+
_literalHeaders = Published(
108+
initialValue: store.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

Lines changed: 2 additions & 1 deletion
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 {
Lines changed: 16 additions & 0 deletions
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+
}
Lines changed: 45 additions & 0 deletions
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+
}
Lines changed: 71 additions & 0 deletions
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+
}
Lines changed: 14 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy