From 55c3ac5c5cd1fb873925969a0dad13f3d5f10c74 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 31 Dec 2017 21:21:09 +0100 Subject: [PATCH] Added delete subcommand #36 --- CHANGELOG.md | 6 + README.md | 11 ++ macosvpn.xcodeproj/project.pbxproj | 18 ++- macosvpn/Classes/VPNArguments.h | 5 +- macosvpn/Classes/VPNArguments.m | 56 ++++++-- macosvpn/Classes/VPNColor.swift | 4 +- macosvpn/Classes/VPNCommandType.swift | 5 + macosvpn/Classes/VPNController.h | 21 --- macosvpn/Classes/VPNController.m | 121 ----------------- macosvpn/Classes/VPNController.swift | 160 +++++++++++++++++++++++ macosvpn/Classes/VPNExitCode.swift | 5 + macosvpn/Classes/VPNHelp.swift | 31 +++-- macosvpn/Classes/VPNServiceCreator.swift | 4 +- macosvpn/Classes/VPNServiceRemover.swift | 98 ++++++++++++++ macosvpn/Info.plist | 2 +- macosvpn/macosvpn-Bridging-Header.h | 1 - macosvpn/macosvpn-Prefix.pch | 4 - spec/features/delete_spec.rb | 11 ++ spec/features/ipsec_spec.rb | 10 +- spec/features/l2tp_spec.rb | 6 + spec/features/version_spec.rb | 2 +- spec/support/macosvpn.rb | 3 +- 22 files changed, 398 insertions(+), 186 deletions(-) create mode 100644 macosvpn/Classes/VPNCommandType.swift delete mode 100644 macosvpn/Classes/VPNController.h delete mode 100644 macosvpn/Classes/VPNController.m create mode 100644 macosvpn/Classes/VPNController.swift create mode 100644 macosvpn/Classes/VPNServiceRemover.swift create mode 100644 spec/features/delete_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b03205..b33efa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # master +# 0.3.4 + +Features: + +* Can delete VPN services, thanks @kooroshh for the request + # 0.3.3 Internal: diff --git a/README.md b/README.md index 2246fb8..3ebd043 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,17 @@ Repeat the arguments for creating multiple Services at once (no matter which sho sudo macosvpn create -leups Atlantic atlantic.example.com Alice p4ssw0rd s3same \\ -leups Northpole northpole.example.com Bob s3cret pr1v4te +#### Deleting VPN services by name + +These commands will prompt you for your password to allow changes to your Network configuration: + + macosvpn delete --name MyVPN --name AnotherOne + macosvpn delete -n ThisOneToo + +Run it with sudo to avoid the prompt: + + sudo macosvpn delete --name MyVPN + ## Troubleshooting * If you're stuck, try to add the `--debug` flag and see if it says something useful. diff --git a/macosvpn.xcodeproj/project.pbxproj b/macosvpn.xcodeproj/project.pbxproj index 09af6db..d328fc7 100644 --- a/macosvpn.xcodeproj/project.pbxproj +++ b/macosvpn.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 324DC058198120B10048BC38 /* VPNController.m in Sources */ = {isa = PBXBuildFile; fileRef = 324DC057198120B10048BC38 /* VPNController.m */; }; 32861394197FED310010BB98 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32861393197FED310010BB98 /* Foundation.framework */; }; 328613A2197FEE7A0010BB98 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 328613A1197FEE7A0010BB98 /* SystemConfiguration.framework */; }; 9D3D847E1982AE1B005BD5F3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D3D847D1982AE1B005BD5F3 /* Security.framework */; }; @@ -102,6 +101,9 @@ C2CE09E41C6E972400B97D6D /* VPNColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CE09E31C6E972400B97D6D /* VPNColor.swift */; }; C2CE09E61C6F643400B97D6D /* VPNLogFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CE09E51C6F643400B97D6D /* VPNLogFormatter.swift */; }; C2CE09E81C6F751300B97D6D /* VPNLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CE09E71C6F751300B97D6D /* VPNLogger.swift */; }; + C2D37FC31FF964EE00B44CA1 /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D37FC21FF964EE00B44CA1 /* VPNController.swift */; }; + C2D37FC51FF9669D00B44CA1 /* VPNCommandType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D37FC41FF9669D00B44CA1 /* VPNCommandType.swift */; }; + C2D37FC71FF970B700B44CA1 /* VPNServiceRemover.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D37FC61FF970B700B44CA1 /* VPNServiceRemover.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -117,8 +119,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 324DC056198120B10048BC38 /* VPNController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VPNController.h; sourceTree = ""; }; - 324DC057198120B10048BC38 /* VPNController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VPNController.m; sourceTree = ""; }; 32861390197FED310010BB98 /* macosvpn */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = macosvpn; sourceTree = BUILT_PRODUCTS_DIR; }; 32861393197FED310010BB98 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 328613A1197FEE7A0010BB98 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; @@ -304,6 +304,9 @@ C2CE09E31C6E972400B97D6D /* VPNColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNColor.swift; sourceTree = ""; }; C2CE09E51C6F643400B97D6D /* VPNLogFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLogFormatter.swift; sourceTree = ""; }; C2CE09E71C6F751300B97D6D /* VPNLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLogger.swift; sourceTree = ""; }; + C2D37FC21FF964EE00B44CA1 /* VPNController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNController.swift; sourceTree = ""; }; + C2D37FC41FF9669D00B44CA1 /* VPNCommandType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNCommandType.swift; sourceTree = ""; }; + C2D37FC61FF970B700B44CA1 /* VPNServiceRemover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNServiceRemover.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -377,8 +380,8 @@ 9D6198CB1981151D00FC01E8 /* VPNArguments.h */, 9D6198CC1981151D00FC01E8 /* VPNArguments.m */, C209587F1E12F970009D2EC3 /* VPNCFArray+Sequence.swift */, - 324DC056198120B10048BC38 /* VPNController.h */, - 324DC057198120B10048BC38 /* VPNController.m */, + C2D37FC21FF964EE00B44CA1 /* VPNController.swift */, + C2D37FC41FF9669D00B44CA1 /* VPNCommandType.swift */, C299053C1E12F9B500570022 /* VPNExitCode.swift */, C233AF801C6E7DCD00F39700 /* VPNHelp.swift */, 9D3D847F1982B017005BD5F3 /* VPNKeychain.h */, @@ -391,6 +394,7 @@ C26157481D9295E6001AD818 /* VPNCommand.swift */, C261574A1D92961C001AD818 /* VPNServiceType.swift */, C26157461D928F29001AD818 /* VPNServiceCreator.swift */, + C2D37FC61FF970B700B44CA1 /* VPNServiceRemover.swift */, C267ADF61ED1F6760058CDE6 /* Optional+String.swift */, ); name = Classes; @@ -680,6 +684,7 @@ 9D619892198112DA00FC01E8 /* CPLALR1Parser.m in Sources */, C267ADF21ED1D7030058CDE6 /* Color ยท Named.swift in Sources */, 9D619894198112DA00FC01E8 /* CPLR1Item.m in Sources */, + C2D37FC31FF964EE00B44CA1 /* VPNController.swift in Sources */, 9D619896198112DA00FC01E8 /* CPLR1Parser.m in Sources */, 9D619898198112DA00FC01E8 /* CPNumberRecogniser.m in Sources */, 9D3D84811982B017005BD5F3 /* VPNKeychain.m in Sources */, @@ -688,7 +693,6 @@ 9D61989E198112DA00FC01E8 /* CPQuotedRecogniser.m in Sources */, C2CE09DB1C6E935600B97D6D /* Color.swift in Sources */, C2CE09DC1C6E935600B97D6D /* ECMA 48.swift in Sources */, - 324DC058198120B10048BC38 /* VPNController.m in Sources */, 9D6198A0198112DA00FC01E8 /* CPQuotedToken.m in Sources */, 9D6198A2198112DA00FC01E8 /* CPRecoveryAction.m in Sources */, 9D6198A5198112DA00FC01E8 /* CPRHSItem.m in Sources */, @@ -722,6 +726,7 @@ 9D619853198112A000FC01E8 /* FSArgsKonstants.m in Sources */, 9D619855198112A000FC01E8 /* FSArgumentPackage.m in Sources */, 9D619858198112A000FC01E8 /* FSArgumentParser.m in Sources */, + C2D37FC51FF9669D00B44CA1 /* VPNCommandType.swift in Sources */, C20958801E12F970009D2EC3 /* VPNCFArray+Sequence.swift in Sources */, C2CE09E61C6F643400B97D6D /* VPNLogFormatter.swift in Sources */, C26157471D928F29001AD818 /* VPNServiceCreator.swift in Sources */, @@ -747,6 +752,7 @@ 9D619873198112A000FC01E8 /* NSProcessInfo+FSArgumentParser.m in Sources */, 9D619875198112A000FC01E8 /* NSScanner+EscapedScanning.m in Sources */, C261574B1D92961C001AD818 /* VPNServiceType.swift in Sources */, + C2D37FC71FF970B700B44CA1 /* VPNServiceRemover.swift in Sources */, 9D619877198112A000FC01E8 /* NSString+Indenter.m in Sources */, 9D61984D1981129900FC01E8 /* main.swift in Sources */, ); diff --git a/macosvpn/Classes/VPNArguments.h b/macosvpn/Classes/VPNArguments.h index 41efc2a..87fcfe7 100644 --- a/macosvpn/Classes/VPNArguments.h +++ b/macosvpn/Classes/VPNArguments.h @@ -14,6 +14,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@class VPNServiceConfig; + @interface VPNArguments : NSObject + (void) setLogLevel; @@ -22,7 +24,8 @@ + (BOOL) versionRequested; + (BOOL) forceRequested; -+ (NSArray*) serviceConfigs; ++ (NSArray*) serviceNames; ++ (NSArray*) serviceConfigs; + (NSUInteger) command; @end diff --git a/macosvpn/Classes/VPNArguments.m b/macosvpn/Classes/VPNArguments.m index 0b29c3b..750bffa 100644 --- a/macosvpn/Classes/VPNArguments.m +++ b/macosvpn/Classes/VPNArguments.m @@ -36,7 +36,7 @@ + (void) setLogLevel { } + (BOOL) helpRequested { - return [self.package booleanValueForSignature: self.helpSig] || [self command] == VPNCommandNone; + return [self.package booleanValueForSignature: self.helpSig] || [self command] == 0; } + (BOOL) versionRequested { @@ -51,14 +51,27 @@ + (NSUInteger) command { if ([[self.package unknownSwitches] count] > 0) DDLogDebug(@"Unknown arguments: %@", [[self.package unknownSwitches] componentsJoinedByString:@" | "]); if ([[self.package uncapturedValues] count] > 0) DDLogDebug(@"Uncaptured argument values: %@", [[self.package uncapturedValues] componentsJoinedByString:@" | "]); - if ([self.package countOfSignature:self.createCommandSig] == VPNCommandCreate) { - return VPNCommandCreate; + if ([self.package countOfSignature:self.createCommandSig] == 1) { + return 1; + } else if ([self.package countOfSignature:self.deleteCommandSig] == 1) { + return 2; } else { - return VPNCommandNone; + return 0; } } -+ (NSArray*) serviceConfigs { ++ (NSArray*) serviceNames { + NSUInteger count = [self.package countOfSignature:self.nameSig]; + NSMutableArray *names = [NSMutableArray arrayWithCapacity:count]; + + for (NSUInteger i = 0; i < count; i++) { + NSString *name = [self extractArgumentForSignature:self.nameSig withFallbackSignature:nil atIndex:i]; + [names addObject:name]; + } + return names; +} + ++ (NSArray*) serviceConfigs { NSArray *l2tpConfigs = [self serviceConfigsForType:VPNServiceL2TPOverIPSec andSignature:self.l2tpSig]; NSArray *ciscoConfigs = [self serviceConfigsForType:VPNServiceCiscoIPSec andSignature:self.ciscoSig]; @@ -137,6 +150,15 @@ + (FSArgumentSignature*) createCommandSig { return command; } ++ (FSArgumentSignature*) deleteCommandSig { + FSArgumentSignature *command = [FSArgumentSignature argumentSignatureWithFormat:@"[delete]"]; + + NSSet *deleteSignatures = [NSSet setWithObjects: self.nameSig, nil]; + + [command setInjectedSignatures:deleteSignatures]; + return command; +} + // Internal: global Argument Flags + (FSArgumentSignature*) helpSig { @@ -199,17 +221,25 @@ + (FSArgumentSignature*) forceSig { return [FSArgumentSignature argumentSignatureWithFormat:@"[-o --force force]"]; } -+ (NSArray*) signatures { - return @[ - self.helpSig, - self.debugSig, - self.versionSig, - self.forceSig, - self.createCommandSig - ]; +// Internal: Delete Configuration Arguments + ++ (FSArgumentSignature*) nameSig { + return [FSArgumentSignature argumentSignatureWithFormat:@"[-n --name name]={1,}"]; } // Wrapping up all valid argument signatures + ++ (NSArray*) signatures { + return @[ + self.helpSig, + self.debugSig, + self.versionSig, + self.forceSig, + self.createCommandSig, + self.deleteCommandSig + ]; + } + + (FSArgumentPackage*) package { return [[NSProcessInfo processInfo] fsargs_parseArgumentsWithSignatures:self.signatures]; } diff --git a/macosvpn/Classes/VPNColor.swift b/macosvpn/Classes/VPNColor.swift index 2148072..f6de6f3 100644 --- a/macosvpn/Classes/VPNColor.swift +++ b/macosvpn/Classes/VPNColor.swift @@ -1,9 +1,9 @@ struct VPNColor { - // https://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg + // https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg static let Blue: UInt8 = 33 static let Green: UInt8 = 35 static let Pink: UInt8 = 199 static let Brown: UInt8 = 130 - + static let Red: UInt8 = 124 } diff --git a/macosvpn/Classes/VPNCommandType.swift b/macosvpn/Classes/VPNCommandType.swift new file mode 100644 index 0000000..3a700c3 --- /dev/null +++ b/macosvpn/Classes/VPNCommandType.swift @@ -0,0 +1,5 @@ +struct VPNCommandType { + static let None: UInt8 = 0 + static let Create: UInt8 = 1 + static let Delete: UInt8 = 2 +} diff --git a/macosvpn/Classes/VPNController.h b/macosvpn/Classes/VPNController.h deleted file mode 100644 index 5b98cff..0000000 --- a/macosvpn/Classes/VPNController.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright (c) 2015 halo. https://github.com/halo/macosvpn - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -@interface VPNController : NSObject - -+ (int) main; - -@end diff --git a/macosvpn/Classes/VPNController.m b/macosvpn/Classes/VPNController.m deleted file mode 100644 index 8badc16..0000000 --- a/macosvpn/Classes/VPNController.m +++ /dev/null @@ -1,121 +0,0 @@ -/* - Copyright (c) 2015 halo. https://github.com/halo/macosvpn - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -// Vendor dependencies -#import - -// Local dependencies -#import "VPNArguments.h" -#import "VPNController.h" -#import "VPNKeychain.h" -#import "VPNServiceConfig.h" - -// This is were the magic happens. -// Exit status codes: 30-59 -@implementation VPNController - -/****************** - * PUBLIC METHODS * - ******************/ - -// The single entry point. This method is executed first. -+ (int) main { - - // Adding the --version flag should never perform anything but showing the version without any blank rows - if ([VPNArguments versionRequested]) return [VPNHelp showVersion]; - - // For readability we print out an empty row before and after. - DDLogInfo(@""); - int exitCode = [self run]; - DDLogInfo(@""); - - // Mention that there were no errors so we can trace bugs more easily. - if (exitCode == 0) { // VPNExitCode.Success - DDLogInfo(@"Finished without errors."); - DDLogInfo(@""); - } - return exitCode; -} - -+ (int) run { - // Adding the --help flag should never perform anything but showing help - if ([VPNArguments helpRequested]) return [VPNHelp showHelp]; - - // To keep this application extensible we introduce different - // commands right from the beginning. We start off with "create" - if ([VPNArguments command] == VPNCommandCreate) { - DDLogDebug(@"So, you wish to create one or more VPN service(s)."); - return [self create]; - - } else { - DDLogError(@"Unknown command. Try --help for instructions."); - return 20; // VPNExitCode.UnknownCommand - } -} - -/******************** - * INTERNAL METHODS * - ********************/ - -// This method is responsible for obtaining authorization in order to perform -// privileged system modifications. It is mandatory for creating network interfaces. -+ (int) create { - - // If this process has root privileges, it will be able to write to the System Keychain. - // If not, we cannot (unless we use a helper tool, which is not the way this application is designed) - // It would be nice to just try to perform the authorization and see if we succeeded or not. - // But the Security System will popup an auth dialog, which is *not* enough to write to the System Keychain. - // So, for now, we will simply bail out unless you called this command line application with the good old `sudo`. - if (getuid() != 0) { - DDLogError(@"Sorry, without superuser privileges I won't be able to write to the System Keychain and thus cannot create a VPN service."); - return 31; // VPNExitCode.PrivilegesRequired - } - - // Obtaining permission to modify network settings - SCPreferencesRef prefs = SCPreferencesCreateWithAuthorization(NULL, CFSTR("macosvpn"), NULL, [VPNAuthorizations create]); - - // Making sure other process cannot make configuration modifications - // by obtaining a system-wide lock over the system preferences. - if (SCPreferencesLock(prefs, TRUE)) { - DDLogDebug(@"Gained superhuman rights."); - } else { - DDLogError(@"Sorry, without superuser privileges I won't be able to add any VPN interfaces."); - return 32; // VPNExitCode.LockingPreferencesFailed - } - - // If everything works out, we will return exit code 0 - int exitCode = 0; - - NSArray *serviceConfigs = [VPNArguments serviceConfigs]; - if (serviceConfigs.count == 0) { - DDLogError(@"You did not specify any interfaces for me to create. Try --help for more information."); - return 22; // VPNExitCode.MissingServices - } - - // Each desired interface configuration will be processed in turn. - // The configuration comes from the command line arguments and is passed on to the create method. - for (VPNServiceConfig *config in serviceConfigs) { - exitCode = (int)[VPNServiceCreator createService:config usingPreferencesRef:prefs]; - // This particular interface could not be created. Let's stop processing the others. - if (exitCode != 0) break; // VPNExitCode.Success - } - - // We're done, other processes may modify the system configuration again - SCPreferencesUnlock(prefs); - return exitCode; -} - -@end diff --git a/macosvpn/Classes/VPNController.swift b/macosvpn/Classes/VPNController.swift new file mode 100644 index 0000000..376d1cb --- /dev/null +++ b/macosvpn/Classes/VPNController.swift @@ -0,0 +1,160 @@ +/* + Copyright (c) 2015 halo. https://github.com/halo/macosvpn + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import SystemConfiguration + +class VPNController { + + class func main() -> Int32 { + // Adding the --version flag should never perform anything but showing the version without any blank rows + if VPNArguments.versionRequested() { + return VPNHelp.showVersion() + } + // For readability we print out an empty row before and after. + DDLogInfo("") + let exitCode: Int32 = self.run() + DDLogInfo("") + // Mention that there were no errors so we can trace bugs more easily. + if exitCode == 0 { + // VPNExitCode.Success + DDLogInfo("Finished without errors.") + DDLogInfo("") + } + return exitCode + } + + class func run() -> Int32 { + // Adding the --help flag should never perform anything but showing help + if VPNArguments.helpRequested() { + return VPNHelp.showHelp() + } + // To keep this application extensible we introduce different + // commands right from the beginning. We start off with "create" + if VPNArguments.command() == VPNCommandType.Create { + DDLogDebug("So, you wish to create one or more VPN service(s).") + return self.create() + } + else if VPNArguments.command() == VPNCommandType.Delete { + DDLogDebug("So, you wish to delete one or more VPN service(s).") + return self.delete() + } + else { + DDLogError("Unknown command. Try --help for instructions.") + return 20 + // VPNExitCode.UnknownCommand + } + } + + /******************** + * INTERNAL METHODS * + ********************/ + + // This method is responsible for obtaining authorization in order to perform + // privileged system modifications. It is mandatory for creating network interfaces. + class func create() -> Int32 { + + // If this process has root privileges, it will be able to write to the System Keychain. + // If not, we cannot (unless we use a helper tool, which is not the way this application is designed) + // It would be nice to just try to perform the authorization and see if we succeeded or not. + // But the Security System will popup an auth dialog, which is *not* enough to write to the System Keychain. + // So, for now, we will simply bail out unless you called this command line application with the good old `sudo`. + if (getuid() != 0) { + DDLogError("Sorry, without superuser privileges I won't be able to write to the System Keychain and thus cannot create a VPN service."); + return 31; // VPNExitCode.PrivilegesRequired + } + + let app = "macosvpn" as CFString + guard let prefs: SCPreferences = SCPreferencesCreateWithAuthorization(nil, app, nil, VPNAuthorizations.create()) else { + DDLogError("Could not create Authorization."); + return 34; // VPNExitCode.AuthorizationCreationFailed + } + + // Making sure other process cannot make configuration modifications + // by obtaining a system-wide lock over the system preferences. + + if (SCPreferencesLock(prefs, true)) { + DDLogDebug("Gained superhuman rights."); + } else { + DDLogError("Sorry, without superuser privileges I won't be able to add any VPN interfaces."); + return 32; // VPNExitCode.LockingPreferencesFailed + } + + // If everything works out, we will return exit code 0 + var exitCode: Int32 = 0; + + let serviceConfigs = VPNArguments.serviceConfigs() + if (serviceConfigs?.count == 0) { + DDLogError("You did not specify any interfaces for me to create. Try --help for more information."); + return 22; // VPNExitCode.MissingServices + } + + // Each desired interface configuration will be processed in turn. + // The configuration comes from the command line arguments and is passed on to the create method. + for config: VPNServiceConfig in serviceConfigs! { + exitCode = Int32(VPNServiceCreator.createService(config, usingPreferencesRef: prefs)) + // This particular interface could not be created. Let's stop processing the others. + if (exitCode != 0) { break; } // VPNExitCode.Success + } + + // We're done, other processes may modify the system configuration again + SCPreferencesUnlock(prefs); + return exitCode; + } + + class func delete() -> Int32 { + DDLogDebug("Shall we delete today?"); + // If everything works out, we will return exit code 0 + var exitCode: Int32 = 0; + + guard let names = VPNArguments.serviceNames() else { + DDLogError("Could not extract service names.") + return VPNExitCode.ServiceNameExtractionFailed; + } + + if (names.count == 0) { + DDLogError("You need to specify at least one --name MyVPNName.") + return 23; // VPNExitCode.MissingNames + } + + let app = "macosvpn" as CFString + guard let prefs: SCPreferences = SCPreferencesCreateWithAuthorization(nil, app, nil, VPNAuthorizations.create()) else { + DDLogError("Could not create Authorization."); + return 34; // VPNExitCode.AuthorizationCreationFailed + } + // Making sure other process cannot make configuration modifications + // by obtaining a system-wide lock over the system preferences. +/* + if (SCPreferencesLock(prefs, true)) { + DDLogDebug("Gained superhuman rights."); + } else { + DDLogError("Sorry, without superuser privileges I won't be able to remove any VPN interfaces."); + return 32; // VPNExitCode.LockingPreferencesFailed + } +*/ + for name: String in names { + exitCode = Int32(VPNServiceRemover.removeService(name, usingPreferencesRef: prefs)) + // This particular interface could not be deleted. Let's stop processing the others. + if (exitCode != 0) { break; } // VPNExitCode.Success + } + + DDLogDebug((names.joined())) + + // We're done, other processes may modify the system configuration again + // SCPreferencesUnlock(prefs); + + return exitCode; + } +} diff --git a/macosvpn/Classes/VPNExitCode.swift b/macosvpn/Classes/VPNExitCode.swift index 8c1259a..957aca2 100644 --- a/macosvpn/Classes/VPNExitCode.swift +++ b/macosvpn/Classes/VPNExitCode.swift @@ -7,10 +7,13 @@ struct VPNExitCode { static let UnknownCommand: Int32 = 20 static let MissingEndpoint: Int32 = 21 static let MissingServices: Int32 = 22 + static let MissingNames: Int32 = 23 + static let ServiceNameExtractionFailed: Int32 = 24 static let PrivilegesRequired: Int32 = 31 static let LockingPreferencesFailed: Int32 = 32 static let NoAuthorization: Int32 = 33 + static let AuthorizationCreationFailed: Int32 = 34 static let UnsupportedInterfaceType: Int32 = 40 static let InterfaceInitializationFailed: Int32 = 41 @@ -34,5 +37,7 @@ struct VPNExitCode { static let CreatingSharedSecretKeychainItemFailed: Int32 = 59 static let CommingingPreferencesFailed: Int32 = 60 static let ApplyingPreferencesFailed: Int32 = 61 + static let RemovingServiceFailed: Int32 = 62 + static let NoServicesRemoved: Int32 = 63 } diff --git a/macosvpn/Classes/VPNHelp.swift b/macosvpn/Classes/VPNHelp.swift index 6aec9e6..4cacc52 100644 --- a/macosvpn/Classes/VPNHelp.swift +++ b/macosvpn/Classes/VPNHelp.swift @@ -20,10 +20,14 @@ open class VPNHelp: NSObject { DDLogDebug("Showing help...") let usage: String = Color.Wrap(styles: .bold).wrap("Usage:") - let command: String = Color.Wrap(foreground: VPNColor.Green).wrap("sudo macosvpn create") - let options: String = Color.Wrap(foreground: VPNColor.Pink).wrap("OPTIONS") - - DDLogInfo("\(usage) \(command) \(options) [OPTIONS AGAIN...]") + let createCommand: String = Color.Wrap(foreground: VPNColor.Green).wrap("sudo macosvpn create") + let createOptions: String = Color.Wrap(foreground: VPNColor.Pink).wrap("OPTIONS") + + let deleteCommand: String = Color.Wrap(foreground: VPNColor.Red).wrap("macosvpn delete") + let deleteOptions: String = Color.Wrap(foreground: VPNColor.Pink).wrap("--name MyVPN") + + DDLogInfo("\(usage) \(createCommand) \(createOptions) [OPTIONS AGAIN...]") + DDLogInfo(" \(deleteCommand) \(deleteOptions) [--name AnotherVPN]") DDLogInfo("") let debugFlag: String = Color.Wrap(foreground: VPNColor.Pink).wrap("--debug") @@ -50,7 +54,8 @@ open class VPNHelp: NSObject { let forceFlag: String = Color.Wrap(foreground: VPNColor.Pink).wrap("--force") let forceFlagShort: String = Color.Wrap(foreground: VPNColor.Pink).wrap("-o") let allShortCiscoFlags: String = Color.Wrap(foreground: VPNColor.Pink).wrap("-ceupsg") - + let nameFlag: String = Color.Wrap(foreground: VPNColor.Pink).wrap("-name") + DDLogInfo("You can always add the \(debugFlag) option for troubleshooting.") DDLogInfo("The \(versionFlag) option displays the current version.") DDLogInfo("Add \(forceFlag) or \(forceFlagShort) to overwrite a VPN that has the same name.") @@ -61,11 +66,11 @@ open class VPNHelp: NSObject { DDLogInfo("") DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("Creating a Cisco IPSec VPN Service")) - DDLogInfo("\(command) \(ciscoFlag) Atlantic \(endpointFlag) atlantic.example.com \(usernameFlag) Alice \(passwordFlag) p4ssw0rd \(sharedSecretFlag) s3same \(groupnameFlag) Dreamteam") + DDLogInfo("\(createCommand) \(ciscoFlag) Atlantic \(endpointFlag) atlantic.example.com \(usernameFlag) Alice \(passwordFlag) p4ssw0rd \(sharedSecretFlag) s3same \(groupnameFlag) Dreamteam") DDLogInfo("") DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("Creating an L2TP over IPSec VPN Service")) - DDLogInfo("\(command) \(l2tpFlag) Atlantic \(endpointFlag) atlantic.example.com \(usernameFlag) Alice \(passwordFlag) p4ssw0rd \(sharedSecretFlag) s3same") + DDLogInfo("\(createCommand) \(l2tpFlag) Atlantic \(endpointFlag) atlantic.example.com \(usernameFlag) Alice \(passwordFlag) p4ssw0rd \(sharedSecretFlag) s3same") DDLogInfo("") DDLogInfo("With L2TP you can") @@ -77,18 +82,22 @@ open class VPNHelp: NSObject { DDLogInfo("") DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("The same command as above but shorter")) - DDLogInfo("\(command) \(ciscoFlagShort) Atlantic \(endpointFlagShort) atlantic.example.com \(usernameFlagShort) Alice \(passwordFlagShort) p4ssw0rd \(sharedSecretFlagShort) s3same \(groupnameFlagShort) Dreamteam") + DDLogInfo("\(createCommand) \(ciscoFlagShort) Atlantic \(endpointFlagShort) atlantic.example.com \(usernameFlagShort) Alice \(passwordFlagShort) p4ssw0rd \(sharedSecretFlagShort) s3same \(groupnameFlagShort) Dreamteam") DDLogInfo("") DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("The same command as short as possible")) - DDLogInfo("\(command) \(allShortCiscoFlags) Atlantic atlantic.example.com Alice p4ssw0rd s3same Dreamteam") + DDLogInfo("\(createCommand) \(allShortCiscoFlags) Atlantic atlantic.example.com Alice p4ssw0rd s3same Dreamteam") DDLogInfo("") DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("Repeat arguments to create multiple VPNs")) - DDLogInfo("\(command) \(allShortCiscoFlags) Atlantic atlantic.example.com Alice p4ssw0rd s3same Dreamteam \\") + DDLogInfo("\(createCommand) \(allShortCiscoFlags) Atlantic atlantic.example.com Alice p4ssw0rd s3same Dreamteam \\") DDLogInfo(" \(allShortCiscoFlags) Northpole northpole.example.com Bob s3cret pr1v4te Spaceteam") DDLogInfo("") - + + DDLogInfo(Color.Wrap(foreground: VPNColor.Blue).wrap("Delete any VPN Service by name")) + DDLogInfo("\(deleteCommand) \(nameFlag) Atlantic") + DDLogInfo("") + DDLogInfo("This application is released under the MIT license.") DDLogInfo("Copyright (c) 2014-\(self.currentYear()) halo.") DDLogInfo(Color.Wrap(foreground: VPNColor.Brown).wrap("https://github.com/halo/macosvpn")) diff --git a/macosvpn/Classes/VPNServiceCreator.swift b/macosvpn/Classes/VPNServiceCreator.swift index 19cb530..db9eaf1 100644 --- a/macosvpn/Classes/VPNServiceCreator.swift +++ b/macosvpn/Classes/VPNServiceCreator.swift @@ -234,11 +234,11 @@ open class VPNServiceCreator: NSObject { DDLogDebug("Commiting all changes including service \(config.name ?? "nil")...") if !SCPreferencesCommitChanges(usingPreferencesRef) { - DDLogError("Error: Could not commit preferences with service. \(config.name ?? "nil"). \(SCErrorString(SCError())) (Code \(SCError()))") + DDLogError("Error: Could not commit preferences with service \(config.name ?? "nil"). \(SCErrorString(SCError())) (Code \(SCError()))") return VPNExitCode.CommingingPreferencesFailed } if !SCPreferencesApplyChanges(usingPreferencesRef) { - DDLogError("Error: Could not apply changes with service. \(config.name ?? "nil"). \(SCErrorString(SCError())) (Code \(SCError()))") + DDLogError("Error: Could not apply changes with service \(config.name ?? "nil"). \(SCErrorString(SCError())) (Code \(SCError()))") return VPNExitCode.ApplyingPreferencesFailed } diff --git a/macosvpn/Classes/VPNServiceRemover.swift b/macosvpn/Classes/VPNServiceRemover.swift new file mode 100644 index 0000000..65026a4 --- /dev/null +++ b/macosvpn/Classes/VPNServiceRemover.swift @@ -0,0 +1,98 @@ +/* + Copyright (c) 2015 halo. https://github.com/halo/macosvpn + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +// Vendor dependencies +import SystemConfiguration + +// This is were the magic happens. +open class VPNServiceRemover { + + /****************** + * PUBLIC METHODS * + ******************/ + + // This method creates one VPN interface according to the desired configuration + open class func removeService(_ name: String, usingPreferencesRef: SCPreferences) -> Int32 { + + DDLogDebug("Removing Service \(name)") + + DDLogDebug("Fetching set of all available network services...") + guard let networkSet = SCNetworkSetCopyCurrent(usingPreferencesRef) else { + DDLogError("Error: Could not fetch current network set when removing \(name). \(SCErrorString(SCError())) (Code \(SCError()))") + return VPNExitCode.CopyingCurrentNetworkSetFailed + } + + guard let services = SCNetworkSetCopyServices(networkSet) else { + DDLogError("Could not retrieve network services set") + return VPNExitCode.CopyingNetworkServicesFailed + } + + var deletedCount: Int = 0 + for serviceInstanceWrapper in services { + let existingService = serviceInstanceWrapper as! SCNetworkService + DDLogDebug("existingService = \(existingService)") + + guard let serviceNameCF = SCNetworkServiceGetName(existingService) else { + DDLogError("SCNetworkServiceGetName failed") + return VPNExitCode.GettingServiceNameFailed + } + + guard let serviceIDCF = SCNetworkServiceGetServiceID(existingService) else { + DDLogError("SCNetworkServiceGetServiceID failed") + return VPNExitCode.GettingServiceIDFailed + } + + let serviceName = serviceNameCF as String + DDLogDebug("serviceName = \(serviceName)") + + let serviceID = serviceIDCF as String + DDLogDebug("serviceID = \(serviceID)") + + if name != serviceName { + DDLogDebug("Ignoring existing Service \(serviceName)") + continue + } + + DDLogDebug("That Service has the ID \(serviceID)") + + if SCNetworkServiceRemove(existingService) { + DDLogInfo("Successfully deleted VPN Service \(name)") + deletedCount += 1; + } else { + DDLogError("Error: Could not remove VPN service \(name) from current network set. \(SCErrorString(SCError())) (Code \(SCError()))") + return VPNExitCode.RemovingServiceFailed + } + } + + if (deletedCount == 0) { + DDLogError("No VPN Service was deleted. Are you sure it exists?") + return VPNExitCode.NoServicesRemoved + } + + DDLogDebug("Commiting all changes including service \(name)...") + if !SCPreferencesCommitChanges(usingPreferencesRef) { + DDLogError("Sorry, without superuser privileges I won't be able to remove any VPN interfaces."); + DDLogDebug("Error: Could not commit preferences after removing service \(name). \(SCErrorString(SCError())) (Code \(SCError()))") + return VPNExitCode.CommingingPreferencesFailed + } + if !SCPreferencesApplyChanges(usingPreferencesRef) { + DDLogError("Error: Could not apply changes after removing \(name). \(SCErrorString(SCError())) (Code \(SCError()))") + return VPNExitCode.ApplyingPreferencesFailed + } + + return VPNExitCode.Success + } +} diff --git a/macosvpn/Info.plist b/macosvpn/Info.plist index 007cb8d..71b6f62 100644 --- a/macosvpn/Info.plist +++ b/macosvpn/Info.plist @@ -3,7 +3,7 @@ CFBundleVersion - 0.3.3 + 0.3.4 CFBundleIdentifier com.funkensturm.macosvpn CFBundleName diff --git a/macosvpn/macosvpn-Bridging-Header.h b/macosvpn/macosvpn-Bridging-Header.h index 3fe3f9d..de8d87d 100644 --- a/macosvpn/macosvpn-Bridging-Header.h +++ b/macosvpn/macosvpn-Bridging-Header.h @@ -2,6 +2,5 @@ #import "CocoaLumberjack.h" #import "VPNArguments.h" -#import "VPNController.h" #import "VPNServiceConfig.h" #import "VPNKeychain.h" diff --git a/macosvpn/macosvpn-Prefix.pch b/macosvpn/macosvpn-Prefix.pch index c9c6550..e936b63 100644 --- a/macosvpn/macosvpn-Prefix.pch +++ b/macosvpn/macosvpn-Prefix.pch @@ -24,10 +24,6 @@ static const DDLogLevel ddLogLevel = DDLogLevelDebug; // static int ddLogLevel = LOG_LEVEL_DEBUG; -// Commands -static int const VPNCommandNone = 0; -static int const VPNCommandCreate = 1; - // Service Types static int const VPNServiceL2TPOverIPSec = 1; static int const VPNServiceCiscoIPSec = 2; diff --git a/spec/features/delete_spec.rb b/spec/features/delete_spec.rb new file mode 100644 index 0000000..645bb7e --- /dev/null +++ b/spec/features/delete_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +RSpec.describe 'Deleting a VPN Service' do + context 'no arguments' do + it 'fails and is informational' do + output, status = Macosvpn.sudo arguments: 'delete' + expect(output).to include ' at least one --name' + expect(status).to eq 23 + end + end +end diff --git a/spec/features/ipsec_spec.rb b/spec/features/ipsec_spec.rb index 451eb61..99dd11a 100644 --- a/spec/features/ipsec_spec.rb +++ b/spec/features/ipsec_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe 'Creating a VPN Service' do +RSpec.describe 'Creating and deleting a VPN Service' do context 'no arguments', :sudo do it 'fails and is informational' do @@ -193,6 +193,14 @@ expect(service.ipsec_local_identifier_type).to be nil expect(service.ipsec_remote_address).to eq 'paris.example.com' expect(service.ipsec_xauth_name).to eq 'Eric' + + # Deleting services with sudo + # It's not possible I think to test the non-sudo mode, which still should work. + arguments = 'delete -n VPNTestIPSec --name VPNTestIPSec2' + output, status = Macosvpn.sudo arguments: arguments + expect(output).to include 'Successfully deleted VPN Service VPNTestIPSec' + expect(output).to include 'Successfully deleted VPN Service VPNTestIPSec2' + expect(status).to eq 0 end end diff --git a/spec/features/l2tp_spec.rb b/spec/features/l2tp_spec.rb index 7000575..90bafeb 100644 --- a/spec/features/l2tp_spec.rb +++ b/spec/features/l2tp_spec.rb @@ -164,6 +164,12 @@ expect(service.ipsec_local_identifier_type).to be nil expect(service.ppp_common_remote_address).to eq 'newyork.example.com' expect(service.ppp_auth_name).to eq 'Eric' + + # Deleting services with sudo + arguments = 'delete -n VPNTestL2TP' + output, status = Macosvpn.sudo arguments: arguments + expect(output).to include 'Successfully deleted VPN Service VPNTestL2TP' + expect(status).to eq 0 end end diff --git a/spec/features/version_spec.rb b/spec/features/version_spec.rb index 9d2ddad..b54413d 100644 --- a/spec/features/version_spec.rb +++ b/spec/features/version_spec.rb @@ -5,7 +5,7 @@ context 'with the --version flag' do it 'shows the Help' do output, status = Macosvpn.call arguments: '--version' - expect(output).to eq "0.3.3\n" + expect(output).to eq "0.3.4\n" expect(status).to eq 10 end end diff --git a/spec/support/macosvpn.rb b/spec/support/macosvpn.rb index 33b5675..8a9b7ab 100644 --- a/spec/support/macosvpn.rb +++ b/spec/support/macosvpn.rb @@ -15,9 +15,10 @@ def self.run(command) [output, status] end + private_class_method :run def self.executable Pathname.new 'build/Release/macosvpn' end - + private_class_method :executable end