From d591dc9518b8f35d87de9c7eac9901ab324db9d0 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 20 Sep 2020 17:44:45 -0700 Subject: [PATCH] Shell setup --- Secretive.xcodeproj/project.pbxproj | 5 ++ .../ShellConfigurationController.swift | 63 ++++++++++++++++++ Secretive/Secretive.entitlements | 4 +- Secretive/Views/ContentView.swift | 49 +++++++------- Secretive/Views/SetupView.swift | 65 ++++++++++++------- 5 files changed, 136 insertions(+), 50 deletions(-) create mode 100644 Secretive/Controllers/ShellConfigurationController.swift diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj index a34060a..852aaa7 100644 --- a/Secretive.xcodeproj/project.pbxproj +++ b/Secretive.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; }; 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; }; + 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -237,6 +238,7 @@ 50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = ""; }; 5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = ""; }; + 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = ""; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = ""; }; 506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -524,6 +526,7 @@ 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */, + 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */, ); path = Controllers; sourceTree = ""; @@ -930,6 +933,7 @@ 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, + 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, @@ -1551,6 +1555,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Secretive/Secretive.entitlements; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; diff --git a/Secretive/Controllers/ShellConfigurationController.swift b/Secretive/Controllers/ShellConfigurationController.swift new file mode 100644 index 0000000..52796b7 --- /dev/null +++ b/Secretive/Controllers/ShellConfigurationController.swift @@ -0,0 +1,63 @@ +import Foundation +import Cocoa + +struct ShellConfigurationController { + + let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String + + var shellInstructions: [ShellConfigInstruction] { + [ + ShellConfigInstruction(shell: "test", + shellConfigDirectory: "~/", + shellConfigFilename: ".test", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + + // ShellConfigInstruction(shell: "zsh", + // shellConfigDirectory: "~/", + // shellConfigFilename: ".zshrc", + // text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "bash", + shellConfigDirectory: "~/", + shellConfigFilename: ".bashrc", + text: "export SSH_AUTH_SOCK=\(socketPath)"), + ShellConfigInstruction(shell: "fish", + shellConfigDirectory: "~/.config/fish", + shellConfigFilename: "config.fish", + text: "set -x SSH_AUTH_SOCK=\(socketPath)"), + ] + + } + + + func addToShell(shellInstructions: ShellConfigInstruction) -> Bool { + let openPanel = NSOpenPanel() + // This is sync, so no need to strongly retain + let delegate = Delegate(name: shellInstructions.shellConfigFilename) + openPanel.delegate = delegate + openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically." + openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)" + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.showsHiddenFiles = true + openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory) + openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename + openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText] + openPanel.runModal() + guard let fileURL = openPanel.urls.first else { return false } + let handle: FileHandle + do { + handle = try FileHandle(forUpdating: fileURL) + guard let existing = try handle.readToEnd(), + let existingString = String(data: existing, encoding: .utf8) else { return false } + guard !existingString.contains(shellInstructions.text) else { + return true + } + try handle.seekToEnd() + } catch { + return false + } + handle.write("\n# Secretive Config\n\(shellInstructions.text)\n".data(using: .utf8)!) + return true + } + +} diff --git a/Secretive/Secretive.entitlements b/Secretive/Secretive.entitlements index 8776520..c1bb5e0 100644 --- a/Secretive/Secretive.entitlements +++ b/Secretive/Secretive.entitlements @@ -4,10 +4,12 @@ com.apple.security.app-sandbox - com.apple.security.smartcard + com.apple.security.files.user-selected.read-write com.apple.security.network.client + com.apple.security.smartcard + keychain-access-groups $(AppIdentifierPrefix)com.maxgoedjen.Secretive diff --git a/Secretive/Views/ContentView.swift b/Secretive/Views/ContentView.swift index 18c1b1c..3cf8bdc 100644 --- a/Secretive/Views/ContentView.swift +++ b/Secretive/Views/ContentView.swift @@ -41,7 +41,7 @@ extension ContentView { var updateNotice: ToolbarItem { guard let update = updater.update else { - return ToolbarItem { AnyView(Spacer()) } + return ToolbarItem { AnyView(EmptyView()) } } let color: Color let text: String @@ -72,7 +72,7 @@ extension ContentView { var newItem: ToolbarItem { guard storeList.modifiableStore?.isAvailable ?? false else { - return ToolbarItem { AnyView(Spacer()) } + return ToolbarItem { AnyView(EmptyView()) } } return ToolbarItem { AnyView( @@ -86,32 +86,31 @@ extension ContentView { } var setupNotice: ToolbarItem { - guard runningSetup || !hasRunSetup || !agentStatusChecker.running else { - return ToolbarItem { AnyView(Spacer()) } - } return ToolbarItem { AnyView( - Button(action: { - runningSetup = true - }, label: { - Group { - if hasRunSetup && !agentStatusChecker.running { - Text("Secret Agent Is Not Running") - } else { - Text("Setup Secretive") - } + Group { + if runningSetup || !hasRunSetup || !agentStatusChecker.running { + Button(action: { + runningSetup = true + }, label: { + Group { + if hasRunSetup && !agentStatusChecker.running { + Text("Secret Agent Is Not Running") + } else { + Text("Setup Secretive") + } + } + .font(.headline) + .foregroundColor(.white) + }) + .background(Color.orange) + .cornerRadius(5) + } else { + EmptyView() } - .font(.headline) - .foregroundColor(.white) - }) - .background(Color.orange) - .cornerRadius(5) - .popover(isPresented: $runningSetup, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { -// SetupView { completed in -// runningSetup = false -// hasRunSetup = completed -// } - SetupView(visible: $runningSetup) + } + .sheet(isPresented: $runningSetup) { + SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) } ) } diff --git a/Secretive/Views/SetupView.swift b/Secretive/Views/SetupView.swift index 26d2a99..d36c035 100644 --- a/Secretive/Views/SetupView.swift +++ b/Secretive/Views/SetupView.swift @@ -4,11 +4,12 @@ struct SetupView: View { @State var stepIndex = 0 @Binding var visible: Bool + @Binding var setupComplete: Bool var body: some View { - GeometryReader { proxy in + GeometryReader { outerProxy in VStack { - StepView(numberOfSteps: 3, currentStep: stepIndex, width: 500) + StepView(numberOfSteps: 3, currentStep: stepIndex, width: 100) GeometryReader { proxy in HStack { SecretAgentSetupView(buttonAction: advance) @@ -17,11 +18,11 @@ struct SetupView: View { .frame(width: proxy.size.width) UpdaterExplainerView { visible = false + setupComplete = true } .frame(width: proxy.size.width) } .offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0) - .animation(.spring()) } } } @@ -30,7 +31,9 @@ struct SetupView: View { func advance() { - stepIndex += 1 + withAnimation(.spring()) { + stepIndex += 1 + } } } @@ -39,7 +42,7 @@ struct SetupView_Previews: PreviewProvider { static var previews: some View { Group { - SetupView(visible: .constant(true)) + SetupView(visible: .constant(true), setupComplete: .constant(false)) } } @@ -60,7 +63,7 @@ struct StepView: View { .frame(height: 5) Rectangle() .foregroundColor(.green) - .frame(width: max(0, (width / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep) - 15), height: 5) + .frame(width: max(0, (width / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep)), height: 5) .animation(.spring()) HStack { ForEach(0.. Void + private static let controller = ShellConfigurationController() + @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! + var body: some View { SetupStepView(title: "Configure your SSH Agent", image: Image(systemName: "terminal"), - bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Drag this into your config file.", - buttonTitle: "Done", + bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file.", + buttonTitle: "I Added it Manually", buttonAction: buttonAction) { Picker(selection: $selectedShellInstruction, label: EmptyView()) { - ForEach(SetupView.Constants.socketPrompts) { instruction in + ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in Text(instruction.shell) .tag(instruction) .padding() } }.pickerStyle(SegmentedPickerStyle()) CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) + Button("Add it For Me") { + let controller = ShellConfigurationController() + if controller.addToShell(shellInstructions: selectedShellInstruction) { + buttonAction() + } + } } } } +class Delegate: NSObject, NSOpenSavePanelDelegate { + + private let name: String + + init(name: String) { + self.name = name + } + + func panel(_ sender: Any, shouldEnable url: URL) -> Bool { + return url.lastPathComponent == name + } + +} + struct SSHAgentSetupView_Previews: PreviewProvider { static var previews: some View { @@ -234,18 +258,6 @@ struct UpdaterExplainerView_Previews: PreviewProvider { extension SetupView { enum Constants { - static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String - static let socketPrompts: [ShellConfigInstruction] = [ - ShellConfigInstruction(shell: "zsh", - shellConfigPath: "~/.zshrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "bash", - shellConfigPath: "~/.bashrc", - text: "export SSH_AUTH_SOCK=\(socketPath)"), - ShellConfigInstruction(shell: "fish", - shellConfigPath: "~/.config/fish/config.fish", - text: "set -x SSH_AUTH_SOCK=\(socketPath)"), - ] static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")! } @@ -254,11 +266,16 @@ extension SetupView { struct ShellConfigInstruction: Identifiable, Hashable { var shell: String - var shellConfigPath: String + var shellConfigDirectory: String + var shellConfigFilename: String var text: String var id: String { shell } + var shellConfigPath: String { + return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename) + } + }