├── .gitignore ├── LICENSE ├── ShareMounter.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── kyle.crawshaw.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── kyle.crawshaw.xcuserdatad │ └── xcschemes │ ├── ShareMounter.xcscheme │ └── xcschememanagement.plist ├── ShareMounter ├── AppDelegate.py ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── ShareMounterAppIcon_128_1x.png │ │ ├── ShareMounterAppIcon_128_2x.png │ │ ├── ShareMounterAppIcon_16_1x.png │ │ ├── ShareMounterAppIcon_16_2x.png │ │ ├── ShareMounterAppIcon_256_1x.png │ │ ├── ShareMounterAppIcon_32_1x.png │ │ ├── ShareMounterAppIcon_32_2x.png │ │ ├── ShareMounterAppIcon_512_1x-1.png │ │ ├── ShareMounterAppIcon_512_1x.png │ │ └── ShareMounterAppIcon_512_2x.png │ ├── Contents.json │ └── DefaultStatusBarIcon.imageset │ │ ├── Contents.json │ │ ├── ShareMounterDefault_21x21.png │ │ └── ShareMounterDefault_41x41.png ├── FoundationPlist.py ├── Info.plist ├── PyDialog.py ├── SMUtilities.py ├── StatusBarController.py ├── en.lproj │ ├── Credits.rtf │ └── MainMenu.xib ├── main.m ├── main.py ├── mount_shares_better.py └── pymacad │ ├── README.md │ ├── __init__.py │ ├── ad │ ├── __init__.py │ └── test_ad.py │ └── kerberos │ └── __init__.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 kylecrawshaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ShareMounter.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BF2568C21BB8D1730098896E /* FoundationPlist.py in Resources */ = {isa = PBXBuildFile; fileRef = BF2568C11BB8D1730098896E /* FoundationPlist.py */; }; 11 | BF2873561BB83C960002D3C4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF2873551BB83C960002D3C4 /* Cocoa.framework */; }; 12 | BF28735E1BB83C960002D3C4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = BF28735D1BB83C960002D3C4 /* main.m */; }; 13 | BF2873611BB83C960002D3C4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = BF28735F1BB83C960002D3C4 /* Credits.rtf */; }; 14 | BF2873641BB83C960002D3C4 /* libpython2.7.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BF2873631BB83C960002D3C4 /* libpython2.7.dylib */; }; 15 | BF2873661BB83C960002D3C4 /* main.py in Resources */ = {isa = PBXBuildFile; fileRef = BF2873651BB83C960002D3C4 /* main.py */; }; 16 | BF2873681BB83C960002D3C4 /* AppDelegate.py in Resources */ = {isa = PBXBuildFile; fileRef = BF2873671BB83C960002D3C4 /* AppDelegate.py */; }; 17 | BF28736B1BB83C960002D3C4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF2873691BB83C960002D3C4 /* MainMenu.xib */; }; 18 | BF28736D1BB83C960002D3C4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF28736C1BB83C960002D3C4 /* Assets.xcassets */; }; 19 | BF2873751BB83D080002D3C4 /* StatusBarController.py in Resources */ = {isa = PBXBuildFile; fileRef = BF2873741BB83D080002D3C4 /* StatusBarController.py */; }; 20 | BF44B40E1BE5DFFB005097F4 /* SMUtilities.py in Resources */ = {isa = PBXBuildFile; fileRef = BF44B40D1BE5DFFB005097F4 /* SMUtilities.py */; }; 21 | BF589B271BEFD0D200CAB6F2 /* pymacad in Resources */ = {isa = PBXBuildFile; fileRef = BF589B261BEFD0D200CAB6F2 /* pymacad */; }; 22 | BF7D5B931BEDB5E6002990CC /* PyDialog.py in Resources */ = {isa = PBXBuildFile; fileRef = BF7D5B921BEDB5E6002990CC /* PyDialog.py */; }; 23 | BFBE20DC1BB8E39700A4A2B2 /* mount_shares_better.py in Resources */ = {isa = PBXBuildFile; fileRef = BFBE20DA1BB8E39700A4A2B2 /* mount_shares_better.py */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | BF2568C11BB8D1730098896E /* FoundationPlist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = FoundationPlist.py; sourceTree = ""; }; 28 | BF2873521BB83C960002D3C4 /* ShareMounter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShareMounter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | BF2873551BB83C960002D3C4 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 30 | BF2873581BB83C960002D3C4 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 31 | BF2873591BB83C960002D3C4 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 32 | BF28735A1BB83C960002D3C4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 33 | BF28735D1BB83C960002D3C4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 34 | BF2873601BB83C960002D3C4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 35 | BF2873631BB83C960002D3C4 /* libpython2.7.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libpython2.7.dylib; path = /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config/libpython2.7.dylib; sourceTree = ""; }; 36 | BF2873651BB83C960002D3C4 /* main.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = main.py; sourceTree = ""; }; 37 | BF2873671BB83C960002D3C4 /* AppDelegate.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = AppDelegate.py; sourceTree = ""; }; 38 | BF28736A1BB83C960002D3C4 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 39 | BF28736C1BB83C960002D3C4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | BF28736E1BB83C960002D3C4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | BF2873741BB83D080002D3C4 /* StatusBarController.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = StatusBarController.py; sourceTree = ""; }; 42 | BF44B40D1BE5DFFB005097F4 /* SMUtilities.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = SMUtilities.py; sourceTree = ""; }; 43 | BF589B261BEFD0D200CAB6F2 /* pymacad */ = {isa = PBXFileReference; lastKnownFileType = folder; path = pymacad; sourceTree = ""; }; 44 | BF7D5B921BEDB5E6002990CC /* PyDialog.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = PyDialog.py; sourceTree = ""; }; 45 | BFBE20DA1BB8E39700A4A2B2 /* mount_shares_better.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = mount_shares_better.py; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | BF28734F1BB83C960002D3C4 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | BF2873641BB83C960002D3C4 /* libpython2.7.dylib in Frameworks */, 54 | BF2873561BB83C960002D3C4 /* Cocoa.framework in Frameworks */, 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | BF2873491BB83C960002D3C4 = { 62 | isa = PBXGroup; 63 | children = ( 64 | BF28735B1BB83C960002D3C4 /* ShareMounter */, 65 | BF2873541BB83C960002D3C4 /* Frameworks */, 66 | BF2873531BB83C960002D3C4 /* Products */, 67 | ); 68 | sourceTree = ""; 69 | }; 70 | BF2873531BB83C960002D3C4 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | BF2873521BB83C960002D3C4 /* ShareMounter.app */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | BF2873541BB83C960002D3C4 /* Frameworks */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | BF2873551BB83C960002D3C4 /* Cocoa.framework */, 82 | BF2873571BB83C960002D3C4 /* Other Frameworks */, 83 | ); 84 | name = Frameworks; 85 | sourceTree = ""; 86 | }; 87 | BF2873571BB83C960002D3C4 /* Other Frameworks */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | BF2873581BB83C960002D3C4 /* AppKit.framework */, 91 | BF2873591BB83C960002D3C4 /* CoreData.framework */, 92 | BF28735A1BB83C960002D3C4 /* Foundation.framework */, 93 | ); 94 | name = "Other Frameworks"; 95 | sourceTree = ""; 96 | }; 97 | BF28735B1BB83C960002D3C4 /* ShareMounter */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | BF589B261BEFD0D200CAB6F2 /* pymacad */, 101 | BF7D5B921BEDB5E6002990CC /* PyDialog.py */, 102 | BF44B40D1BE5DFFB005097F4 /* SMUtilities.py */, 103 | BFBE20DA1BB8E39700A4A2B2 /* mount_shares_better.py */, 104 | BF2568C11BB8D1730098896E /* FoundationPlist.py */, 105 | BF2873651BB83C960002D3C4 /* main.py */, 106 | BF2873671BB83C960002D3C4 /* AppDelegate.py */, 107 | BF2873691BB83C960002D3C4 /* MainMenu.xib */, 108 | BF28736C1BB83C960002D3C4 /* Assets.xcassets */, 109 | BF28736E1BB83C960002D3C4 /* Info.plist */, 110 | BF2873621BB83C960002D3C4 /* SharedLibraries */, 111 | BF28735C1BB83C960002D3C4 /* Supporting Files */, 112 | BF2873741BB83D080002D3C4 /* StatusBarController.py */, 113 | ); 114 | path = ShareMounter; 115 | sourceTree = ""; 116 | }; 117 | BF28735C1BB83C960002D3C4 /* Supporting Files */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | BF28735D1BB83C960002D3C4 /* main.m */, 121 | BF28735F1BB83C960002D3C4 /* Credits.rtf */, 122 | ); 123 | name = "Supporting Files"; 124 | sourceTree = ""; 125 | }; 126 | BF2873621BB83C960002D3C4 /* SharedLibraries */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | BF2873631BB83C960002D3C4 /* libpython2.7.dylib */, 130 | ); 131 | name = SharedLibraries; 132 | sourceTree = ""; 133 | }; 134 | /* End PBXGroup section */ 135 | 136 | /* Begin PBXNativeTarget section */ 137 | BF2873511BB83C960002D3C4 /* ShareMounter */ = { 138 | isa = PBXNativeTarget; 139 | buildConfigurationList = BF2873711BB83C960002D3C4 /* Build configuration list for PBXNativeTarget "ShareMounter" */; 140 | buildPhases = ( 141 | BF28734E1BB83C960002D3C4 /* Sources */, 142 | BF28734F1BB83C960002D3C4 /* Frameworks */, 143 | BF2873501BB83C960002D3C4 /* Resources */, 144 | ); 145 | buildRules = ( 146 | ); 147 | dependencies = ( 148 | ); 149 | name = ShareMounter; 150 | productName = ShareMounter; 151 | productReference = BF2873521BB83C960002D3C4 /* ShareMounter.app */; 152 | productType = "com.apple.product-type.application"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | BF28734A1BB83C960002D3C4 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | LastUpgradeCheck = 0700; 161 | ORGANIZATIONNAME = "Kyle Crawshaw"; 162 | TargetAttributes = { 163 | BF2873511BB83C960002D3C4 = { 164 | CreatedOnToolsVersion = 7.0; 165 | DevelopmentTeam = H736X34P6B; 166 | }; 167 | }; 168 | }; 169 | buildConfigurationList = BF28734D1BB83C960002D3C4 /* Build configuration list for PBXProject "ShareMounter" */; 170 | compatibilityVersion = "Xcode 3.2"; 171 | developmentRegion = English; 172 | hasScannedForEncodings = 0; 173 | knownRegions = ( 174 | en, 175 | ); 176 | mainGroup = BF2873491BB83C960002D3C4; 177 | productRefGroup = BF2873531BB83C960002D3C4 /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | BF2873511BB83C960002D3C4 /* ShareMounter */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | BF2873501BB83C960002D3C4 /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | BF2873611BB83C960002D3C4 /* Credits.rtf in Resources */, 192 | BF2873681BB83C960002D3C4 /* AppDelegate.py in Resources */, 193 | BF2873661BB83C960002D3C4 /* main.py in Resources */, 194 | BF7D5B931BEDB5E6002990CC /* PyDialog.py in Resources */, 195 | BFBE20DC1BB8E39700A4A2B2 /* mount_shares_better.py in Resources */, 196 | BF44B40E1BE5DFFB005097F4 /* SMUtilities.py in Resources */, 197 | BF28736B1BB83C960002D3C4 /* MainMenu.xib in Resources */, 198 | BF589B271BEFD0D200CAB6F2 /* pymacad in Resources */, 199 | BF2568C21BB8D1730098896E /* FoundationPlist.py in Resources */, 200 | BF2873751BB83D080002D3C4 /* StatusBarController.py in Resources */, 201 | BF28736D1BB83C960002D3C4 /* Assets.xcassets in Resources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXResourcesBuildPhase section */ 206 | 207 | /* Begin PBXSourcesBuildPhase section */ 208 | BF28734E1BB83C960002D3C4 /* Sources */ = { 209 | isa = PBXSourcesBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | BF28735E1BB83C960002D3C4 /* main.m in Sources */, 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXSourcesBuildPhase section */ 217 | 218 | /* Begin PBXVariantGroup section */ 219 | BF28735F1BB83C960002D3C4 /* Credits.rtf */ = { 220 | isa = PBXVariantGroup; 221 | children = ( 222 | BF2873601BB83C960002D3C4 /* en */, 223 | ); 224 | name = Credits.rtf; 225 | sourceTree = ""; 226 | }; 227 | BF2873691BB83C960002D3C4 /* MainMenu.xib */ = { 228 | isa = PBXVariantGroup; 229 | children = ( 230 | BF28736A1BB83C960002D3C4 /* en */, 231 | ); 232 | name = MainMenu.xib; 233 | sourceTree = ""; 234 | }; 235 | /* End PBXVariantGroup section */ 236 | 237 | /* Begin XCBuildConfiguration section */ 238 | BF28736F1BB83C960002D3C4 /* Debug */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_UNREACHABLE_CODE = YES; 254 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 255 | CODE_SIGN_IDENTITY = "-"; 256 | COPY_PHASE_STRIP = NO; 257 | DEBUG_INFORMATION_FORMAT = dwarf; 258 | ENABLE_STRICT_OBJC_MSGSEND = YES; 259 | ENABLE_TESTABILITY = YES; 260 | GCC_C_LANGUAGE_STANDARD = gnu99; 261 | GCC_DYNAMIC_NO_PIC = NO; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_OPTIMIZATION_LEVEL = 0; 264 | GCC_PREPROCESSOR_DEFINITIONS = ( 265 | "DEBUG=1", 266 | "$(inherited)", 267 | ); 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | HEADER_SEARCH_PATHS = ( 275 | "$(inherited)", 276 | /System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7, 277 | ); 278 | LIBRARY_SEARCH_PATHS = ( 279 | "$(inherited)", 280 | "$(SYSTEM_LIBRARY_DIR)/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config", 281 | ); 282 | MACOSX_DEPLOYMENT_TARGET = 10.8; 283 | MTL_ENABLE_DEBUG_INFO = YES; 284 | ONLY_ACTIVE_ARCH = YES; 285 | SDKROOT = macosx; 286 | }; 287 | name = Debug; 288 | }; 289 | BF2873701BB83C960002D3C4 /* Release */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ALWAYS_SEARCH_USER_PATHS = NO; 293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 294 | CLANG_CXX_LIBRARY = "libc++"; 295 | CLANG_ENABLE_MODULES = YES; 296 | CLANG_ENABLE_OBJC_ARC = YES; 297 | CLANG_WARN_BOOL_CONVERSION = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 300 | CLANG_WARN_EMPTY_BODY = YES; 301 | CLANG_WARN_ENUM_CONVERSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 304 | CLANG_WARN_UNREACHABLE_CODE = YES; 305 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 306 | CODE_SIGN_IDENTITY = "-"; 307 | COPY_PHASE_STRIP = NO; 308 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 309 | ENABLE_NS_ASSERTIONS = NO; 310 | ENABLE_STRICT_OBJC_MSGSEND = YES; 311 | GCC_C_LANGUAGE_STANDARD = gnu99; 312 | GCC_NO_COMMON_BLOCKS = YES; 313 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 314 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 315 | GCC_WARN_UNDECLARED_SELECTOR = YES; 316 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 317 | GCC_WARN_UNUSED_FUNCTION = YES; 318 | GCC_WARN_UNUSED_VARIABLE = YES; 319 | HEADER_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | /System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7, 322 | ); 323 | LIBRARY_SEARCH_PATHS = ( 324 | "$(inherited)", 325 | "$(SYSTEM_LIBRARY_DIR)/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config", 326 | ); 327 | MACOSX_DEPLOYMENT_TARGET = 10.8; 328 | MTL_ENABLE_DEBUG_INFO = NO; 329 | SDKROOT = macosx; 330 | }; 331 | name = Release; 332 | }; 333 | BF2873721BB83C960002D3C4 /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 337 | CODE_SIGN_IDENTITY = ""; 338 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; 339 | COMBINE_HIDPI_IMAGES = YES; 340 | INFOPLIST_FILE = ShareMounter/Info.plist; 341 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 342 | LIBRARY_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "$(SYSTEM_LIBRARY_DIR)/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config", 345 | ); 346 | PRODUCT_BUNDLE_IDENTIFIER = com.github.kylecrawshaw.ShareMounter; 347 | PRODUCT_NAME = "$(TARGET_NAME)"; 348 | PROVISIONING_PROFILE = ""; 349 | }; 350 | name = Debug; 351 | }; 352 | BF2873731BB83C960002D3C4 /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 356 | CODE_SIGN_IDENTITY = ""; 357 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = ""; 358 | COMBINE_HIDPI_IMAGES = YES; 359 | INFOPLIST_FILE = ShareMounter/Info.plist; 360 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 361 | LIBRARY_SEARCH_PATHS = ( 362 | "$(inherited)", 363 | "$(SYSTEM_LIBRARY_DIR)/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config", 364 | ); 365 | PRODUCT_BUNDLE_IDENTIFIER = com.github.kylecrawshaw.ShareMounter; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | PROVISIONING_PROFILE = ""; 368 | }; 369 | name = Release; 370 | }; 371 | /* End XCBuildConfiguration section */ 372 | 373 | /* Begin XCConfigurationList section */ 374 | BF28734D1BB83C960002D3C4 /* Build configuration list for PBXProject "ShareMounter" */ = { 375 | isa = XCConfigurationList; 376 | buildConfigurations = ( 377 | BF28736F1BB83C960002D3C4 /* Debug */, 378 | BF2873701BB83C960002D3C4 /* Release */, 379 | ); 380 | defaultConfigurationIsVisible = 0; 381 | defaultConfigurationName = Release; 382 | }; 383 | BF2873711BB83C960002D3C4 /* Build configuration list for PBXNativeTarget "ShareMounter" */ = { 384 | isa = XCConfigurationList; 385 | buildConfigurations = ( 386 | BF2873721BB83C960002D3C4 /* Debug */, 387 | BF2873731BB83C960002D3C4 /* Release */, 388 | ); 389 | defaultConfigurationIsVisible = 0; 390 | defaultConfigurationName = Release; 391 | }; 392 | /* End XCConfigurationList section */ 393 | }; 394 | rootObject = BF28734A1BB83C960002D3C4 /* Project object */; 395 | } 396 | -------------------------------------------------------------------------------- /ShareMounter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ShareMounter.xcodeproj/project.xcworkspace/xcuserdata/kyle.crawshaw.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter.xcodeproj/project.xcworkspace/xcuserdata/kyle.crawshaw.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ShareMounter.xcodeproj/xcuserdata/kyle.crawshaw.xcuserdatad/xcschemes/ShareMounter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ShareMounter.xcodeproj/xcuserdata/kyle.crawshaw.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ShareMounter.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | BF2873511BB83C960002D3C4 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ShareMounter/AppDelegate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # AppDelegate.py 4 | # ShareMounter 5 | # 6 | # Created by Mr. Kyle Crawshaw on 9/27/15. 7 | # Copyright (c) 2015 Kyle Crawshaw. All rights reserved. 8 | # 9 | 10 | from Foundation import * 11 | from AppKit import * 12 | import objc 13 | 14 | class AppDelegate(NSObject): 15 | 16 | statusBarController = objc.IBOutlet() 17 | 18 | def applicationDidFinishLaunching_(self, notification): 19 | NSLog("Application did finish launching.") 20 | if self.statusBarController: 21 | self.statusBarController.runStartup() 22 | nc = NSUserNotificationCenter.defaultUserNotificationCenter() 23 | nc.setDelegate_(self) 24 | 25 | def userNotificationCenter_shouldPresentNotification_(self, center, notification): 26 | return objc.YES 27 | 28 | def applicationWillTerminate_(self, notification): 29 | # be nice and remove our observers from NSWorkspace 30 | nc = NSWorkspace.sharedWorkspace().notificationCenter() 31 | nc.removeObserver_(self.statusBarController) 32 | self.statusBarController.releaseStatusBar() 33 | -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "ShareMounterAppIcon_16_1x.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "ShareMounterAppIcon_16_2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "ShareMounterAppIcon_32_1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "ShareMounterAppIcon_32_2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "ShareMounterAppIcon_128_1x.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "ShareMounterAppIcon_128_2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "ShareMounterAppIcon_256_1x.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "ShareMounterAppIcon_512_1x-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "ShareMounterAppIcon_512_1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "ShareMounterAppIcon_512_2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_128_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_128_1x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_128_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_128_2x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_16_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_16_1x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_16_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_16_2x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_256_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_256_1x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_32_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_32_1x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_32_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_32_2x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_1x-1.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_1x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/AppIcon.appiconset/ShareMounterAppIcon_512_2x.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/DefaultStatusBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ShareMounterDefault_21x21.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ShareMounterDefault_41x41.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/DefaultStatusBarIcon.imageset/ShareMounterDefault_21x21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/DefaultStatusBarIcon.imageset/ShareMounterDefault_21x21.png -------------------------------------------------------------------------------- /ShareMounter/Assets.xcassets/DefaultStatusBarIcon.imageset/ShareMounterDefault_41x41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylecrawshaw/ShareMounter/2b3248d1f236b3902b493f83b58f2d530a9fce38/ShareMounter/Assets.xcassets/DefaultStatusBarIcon.imageset/ShareMounterDefault_41x41.png -------------------------------------------------------------------------------- /ShareMounter/FoundationPlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | # 4 | # Copyright 2009-2014 Greg Neagle. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | """FoundationPlist.py -- a tool to generate and parse MacOSX .plist files. 18 | 19 | This is intended as a drop-in replacement for Python's included plistlib, 20 | with a few caveats: 21 | - readPlist() and writePlist() operate only on a filepath, 22 | not a file object. 23 | - there is no support for the deprecated functions: 24 | readPlistFromResource() 25 | writePlistToResource() 26 | - there is no support for the deprecated Plist class. 27 | 28 | The Property List (.plist) file format is a simple XML pickle supporting 29 | basic object types, like dictionaries, lists, numbers and strings. 30 | Usually the top level object is a dictionary. 31 | 32 | To write out a plist file, use the writePlist(rootObject, filepath) 33 | function. 'rootObject' is the top level object, 'filepath' is a 34 | filename. 35 | 36 | To parse a plist from a file, use the readPlist(filepath) function, 37 | with a file name. It returns the top level object (again, usually a 38 | dictionary). 39 | 40 | To work with plist data in strings, you can use readPlistFromString() 41 | and writePlistToString(). 42 | """ 43 | 44 | # PyLint cannot properly find names inside Cocoa libraries, so issues bogus 45 | # No name 'Foo' in module 'Bar' warnings. Disable them. 46 | # pylint: disable=E0611 47 | from Foundation import NSData 48 | from Foundation import NSPropertyListSerialization 49 | from Foundation import NSPropertyListMutableContainers 50 | from Foundation import NSPropertyListXMLFormat_v1_0 51 | # pylint: enable=E0611 52 | 53 | # Disable PyLint complaining about 'invalid' camelCase names 54 | # pylint: disable=C0103 55 | 56 | 57 | class FoundationPlistException(Exception): 58 | """Basic exception for plist errors""" 59 | pass 60 | 61 | class NSPropertyListSerializationException(FoundationPlistException): 62 | """Read/parse error for plists""" 63 | pass 64 | 65 | class NSPropertyListWriteException(FoundationPlistException): 66 | """Write error for plists""" 67 | pass 68 | 69 | def readPlist(filepath): 70 | """ 71 | Read a .plist file from filepath. Return the unpacked root object 72 | (which is usually a dictionary). 73 | """ 74 | plistData = NSData.dataWithContentsOfFile_(filepath) 75 | dataObject, dummy_plistFormat, error = ( 76 | NSPropertyListSerialization. 77 | propertyListFromData_mutabilityOption_format_errorDescription_( 78 | plistData, NSPropertyListMutableContainers, None, None)) 79 | if dataObject is None: 80 | if error: 81 | error = error.encode('ascii', 'ignore') 82 | else: 83 | error = "Unknown error" 84 | errmsg = "%s in file %s" % (error, filepath) 85 | raise NSPropertyListSerializationException(errmsg) 86 | else: 87 | return dataObject 88 | 89 | 90 | def readPlistFromString(data): 91 | '''Read a plist data from a string. Return the root object.''' 92 | try: 93 | plistData = buffer(data) 94 | except TypeError, err: 95 | raise NSPropertyListSerializationException(err) 96 | dataObject, dummy_plistFormat, error = ( 97 | NSPropertyListSerialization. 98 | propertyListFromData_mutabilityOption_format_errorDescription_( 99 | plistData, NSPropertyListMutableContainers, None, None)) 100 | if dataObject is None: 101 | if error: 102 | error = error.encode('ascii', 'ignore') 103 | else: 104 | error = "Unknown error" 105 | raise NSPropertyListSerializationException(error) 106 | else: 107 | return dataObject 108 | 109 | 110 | def writePlist(dataObject, filepath): 111 | ''' 112 | Write 'rootObject' as a plist to filepath. 113 | ''' 114 | plistData, error = ( 115 | NSPropertyListSerialization. 116 | dataFromPropertyList_format_errorDescription_( 117 | dataObject, NSPropertyListXMLFormat_v1_0, None)) 118 | if plistData is None: 119 | if error: 120 | error = error.encode('ascii', 'ignore') 121 | else: 122 | error = "Unknown error" 123 | raise NSPropertyListSerializationException(error) 124 | else: 125 | if plistData.writeToFile_atomically_(filepath, True): 126 | return 127 | else: 128 | raise NSPropertyListWriteException( 129 | "Failed to write plist data to %s" % filepath) 130 | 131 | 132 | def writePlistToString(rootObject): 133 | '''Return 'rootObject' as a plist-formatted string.''' 134 | plistData, error = ( 135 | NSPropertyListSerialization. 136 | dataFromPropertyList_format_errorDescription_( 137 | rootObject, NSPropertyListXMLFormat_v1_0, None)) 138 | if plistData is None: 139 | if error: 140 | error = error.encode('ascii', 'ignore') 141 | else: 142 | error = "Unknown error" 143 | raise NSPropertyListSerializationException(error) 144 | else: 145 | return str(plistData) 146 | 147 | -------------------------------------------------------------------------------- /ShareMounter/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | AppIcon.icns 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.5 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2015 Kyle Crawshaw. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /ShareMounter/PyDialog.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSTextField, NSMakeRect, NSRect, NSSecureTextField, NSAlert, \ 2 | NSCriticalAlertStyle, NSView, NSButton, NSSwitchButton 3 | 4 | class AlertDialog(object): 5 | 6 | def __init__(self, title, message): 7 | self.alert = NSAlert.alloc().init() 8 | self.alert.setTitle_andMessage_(title, message) 9 | self.button_return = None 10 | 11 | def display(self): 12 | self.button_return = self.alert.runModal() 13 | 14 | class InputDialog(AlertDialog): 15 | 16 | def __init__(self, title, message): 17 | AlertDialog.__init__(self, title, message) 18 | self.input = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 0, 200, 24)) 19 | self.alert.setAccessoryView_(self.input) 20 | 21 | def get_input(self): 22 | return self.input.stringValue() 23 | 24 | class SecureInputDialog(InputDialog): 25 | 26 | def __init__(self, title, message): 27 | InputDialog.__init__(self, title, message) 28 | self.input = NSSecureTextField.alloc().initWithFrame_(NSMakeRect(0, 0, 200, 24)) 29 | self.alert.setAccessoryView_(self.input) 30 | 31 | class ContinueDialog(AlertDialog): 32 | 33 | def __init__(self, title, message): 34 | AlertDialog.__init__(self, title, message) 35 | self.alert.addButtonWithTitle_('OK') 36 | self.alert.addButtonWithTitle_('Cancel') 37 | self.alert.setAlertStyle_(NSCriticalAlertStyle) 38 | 39 | def should_continue(self): 40 | return True if self.button_return == 1000 else False 41 | 42 | class PasswordDialog(object): 43 | 44 | def __init__(self): 45 | ''' initializes an alert with custom view containing username and 46 | password fields with a save to keychain checkbox''' 47 | # Create an dialog with ok and cancel buttons 48 | self.alert = NSAlert.alloc().init() 49 | self.alert.setMessageText_('Please enter your username and password!') 50 | self.alert.addButtonWithTitle_('Ok') 51 | self.alert.addButtonWithTitle_('Cancel') 52 | 53 | # create the view for username and password fields 54 | accessory_view = NSView.alloc().initWithFrame_(NSMakeRect(0, 114, 250, 110)) 55 | 56 | # setup username field and label 57 | self.username_field = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 70, 250, 22)) 58 | username_label = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 94, 250, 20)) 59 | username_label.setStringValue_('Username:') 60 | username_label.setBezeled_(False) 61 | username_label.setDrawsBackground_(False) 62 | username_label.setEditable_(False) 63 | username_label.setSelectable_(False) 64 | 65 | # setup password field and label 66 | self.password_field = NSSecureTextField.alloc().initWithFrame_(NSMakeRect(0, 24, 250, 22)) 67 | password_label = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 48, 250, 20)) 68 | password_label.setStringValue_('Password:') 69 | password_label.setBezeled_(False) 70 | password_label.setDrawsBackground_(False) 71 | password_label.setEditable_(False) 72 | password_label.setSelectable_(False) 73 | 74 | # setup keychain checkbox and label 75 | self.keychain_checkbox = NSButton.alloc().initWithFrame_(NSMakeRect(0, 0, 200, 20)) 76 | self.keychain_checkbox.setButtonType_(NSSwitchButton) 77 | self.keychain_checkbox.cell().setTitle_('Save to Keychain') 78 | self.keychain_checkbox.cell().setBordered_(False) 79 | self.keychain_checkbox.cell().setEnabled_(True) 80 | self.keychain_checkbox.cell().setState_(True) 81 | 82 | # add various objects as subviews 83 | accessory_view.addSubview_(self.keychain_checkbox) 84 | accessory_view.addSubview_(username_label) 85 | accessory_view.addSubview_(self.username_field) 86 | accessory_view.addSubview_(password_label) 87 | accessory_view.addSubview_(self.password_field) 88 | 89 | # add custom view to alert dialog 90 | self.alert.setAccessoryView_(accessory_view) 91 | 92 | 93 | def display(self): 94 | ''' displays the dialog and returns True is user clicked "ok" or 95 | False if user clicked "cancel"''' 96 | self.response = True if self.alert.runModal() == 1000 else False 97 | return self.response 98 | 99 | 100 | def username(self): 101 | ''' return the value of the username field''' 102 | return self.username_field.stringValue() 103 | 104 | 105 | def password(self): 106 | ''' return the value of the password field''' 107 | return self.password_field.stringValue() 108 | 109 | 110 | def save(self): 111 | ''' return True if checkbox if selected ''' 112 | return True if self.keychain_checkbox.state() else False 113 | -------------------------------------------------------------------------------- /ShareMounter/SMUtilities.py: -------------------------------------------------------------------------------- 1 | #pylint: disable=E0611 2 | from SystemConfiguration import SCDynamicStoreCreate, \ 3 | SCDynamicStoreCopyValue, \ 4 | SCDynamicStoreCopyConsoleUser 5 | from Foundation import CFPreferencesCopyAppValue, CFPreferencesAppSynchronize, \ 6 | CFPreferencesSetAppValue, NSFileManager, NSLog 7 | from AppKit import NSWorkspace, NSUserNotificationCenter, NSUserNotification, \ 8 | NSURL 9 | import FoundationPlist 10 | import os, subprocess, urlparse 11 | import mount_shares_better 12 | import threading 13 | import PyDialog 14 | from pymacad import kerberos, ad 15 | from Cocoa import NSAppleScript 16 | # import requests 17 | 18 | homedir = os.path.expanduser('~') 19 | user_preferences_path = os.path.join(homedir, 'Library/Preferences/ShareMounter.plist') 20 | global_preferences_path = '/Library/Preferences/ShareMounter.plist' 21 | kCFPreferencesCurrentApplication = 'ShareMounter' 22 | 23 | 24 | def is_ldap_reachable(domain): 25 | '''Checks whether or not the ldap server can be reached. Returns True.''' 26 | try: 27 | # cmd = ['dig', '-t', 'srv', '_ldap._tcp.{}'.format(domain), '+time=1', '+tries=3'] 28 | # dig = subprocess.check_output(cmd) 29 | # if 'ANSWER SECTION' in dig: 30 | if ad.accessible(domain): 31 | NSLog('Ldap server is reachable by dig') 32 | return True 33 | else: 34 | NSLog('Ldap server is not reachable by dig') 35 | return False 36 | except subprocess.CalledProcessError: 37 | NSLog('Ldap server is not reachable by dig') 38 | return False 39 | 40 | 41 | def is_network_volume(share_path): 42 | '''NSWorkspace alows us to check for filesystem type of a specified path''' 43 | #pylint: disable=C0301,C0103 44 | ws = NSWorkspace.alloc().init() 45 | share_type = ws.getFileSystemInfoForPath_isRemovable_isWritable_isUnmountable_description_type_(share_path, 46 | None, 47 | None, 48 | None, 49 | None, 50 | None)[-1] 51 | return True if share_type == 'smbfs' or share_type == 'webdav' else False 52 | #pylint: enable=C0301,C0103 53 | 54 | 55 | def get_mounted_network_volumes(): 56 | '''Uses Foundation.NSFileManager to get mounted volumes. is_network_volume() is called 57 | to filter out volumes that are not of type "smbfs"''' 58 | #pylint: disable=C0103 59 | fm = NSFileManager.alloc().init() 60 | mounts = fm.mountedVolumeURLsIncludingResourceValuesForKeys_options_(None, 0) 61 | mount_paths = [] 62 | for mount in mounts: 63 | mount_path = mount.fileSystemRepresentation() 64 | if is_network_volume(mount_path): 65 | mount_paths.append(mount_path) 66 | return mount_paths 67 | #pylint: enable=C0103 68 | 69 | def mount_share(share_url): 70 | thread = CustomThread(url=share_url) 71 | thread.daemon = True 72 | thread.start() 73 | 74 | 75 | def unmount_share(mount_path): 76 | thread = CustomThread(unmount=mount_path) 77 | thread.daemon = True 78 | thread.start() 79 | 80 | 81 | def _unmount_share_cmd(mount_path): 82 | '''Unmounts share using hdiutil, would like to use something other than subprocess''' 83 | out = subprocess.check_output(['/usr/bin/hdiutil', 'unmount', mount_path]) 84 | return out 85 | 86 | 87 | def open_file(file_path): 88 | '''Opens file/folder at specified path''' 89 | file_url = NSURL.fileURLWithPath_(file_path) 90 | NSWorkspace.sharedWorkspace().openURL_(file_url) 91 | NSLog('User opened {0}'.format(file_path)) 92 | 93 | 94 | def notify(title, subject): 95 | '''Displays a notification if user has not disable ShareMounter in 96 | notification center''' 97 | if read_pref('display_notifications'): 98 | notification = NSUserNotification.alloc().init() 99 | notification.setTitle_(title) 100 | notification.setInformativeText_(subject) 101 | notification.setSoundName_('NSUserNotificationDefaultSoundName') 102 | NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification_(notification) 103 | 104 | 105 | def _get_console_user(): 106 | return SCDynamicStoreCopyConsoleUser(None, None, None)[0] 107 | 108 | 109 | def write_pref(key, value): 110 | # NSLog('Setting "{0}" to "{1}"'.format(key, value)) 111 | CFPreferencesSetAppValue(key, value, kCFPreferencesCurrentApplication) 112 | if not CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication): 113 | d = PyDialog.AlertDialog('Something went wrong...', 114 | 'Unable to save preference: "{0}"'.format(key)) 115 | d.display() 116 | NSLog('ERROR: unable to save user preferences!') 117 | 118 | def read_pref(pref_key): 119 | return CFPreferencesCopyAppValue(pref_key, kCFPreferencesCurrentApplication) 120 | 121 | 122 | def get_managed_shares(): 123 | managed_shares = list(read_pref('managed_shares')) 124 | return [dict(share) for share in managed_shares] 125 | 126 | def get_user_added_shares(): 127 | user_added_shares = list(read_pref('user_added_shares')) 128 | return [dict(share) for share in user_added_shares] 129 | 130 | 131 | class ConfigManager(object): 132 | 133 | def __init__(self): 134 | self.load_prefs() 135 | 136 | def validate_kerberos(self): 137 | NSLog('Checking and updating Kerberos if necessary...') 138 | def _update_login(): 139 | try: 140 | if not ad.bound(): 141 | d = PyDialog.PasswordDialog() 142 | selection = d.display() 143 | if selection: 144 | principal = ad._format_principal(d.username()) 145 | domain = ad._split_principal(d.username())[1] 146 | write_pref('principal', principal) 147 | write_pref('domain', domain) 148 | if is_ldap_reachable(read_pref('domain')): 149 | result = kerberos.test_kerberos_password(read_pref('principal'), 150 | d.password()) 151 | if result != True: 152 | _update_login() 153 | else: 154 | if is_ldap_reachable(read_pref('domain')): 155 | success = kerberos.test_kerberos_password(read_pref('principal'), _update_password()) 156 | if not success: 157 | _update_login() 158 | except ad.PrincipalFormatError: 159 | message = 'Username must be formatted as user@domain.com' 160 | username_dialog = PyDialog.AlertDialog('Invalid username!', 161 | message) 162 | username_dialog.display() 163 | self.validate_kerberos() 164 | 165 | def _update_password(): 166 | message = 'Enter the password for {0}'.format(read_pref('principal')) 167 | d = PyDialog.SecureInputDialog('Could not find keychain entry!', message) 168 | d.display() 169 | return d.get_input() 170 | 171 | 172 | if not read_pref('domain') or not read_pref('principal'): 173 | _update_login() 174 | 175 | 176 | if is_ldap_reachable(read_pref('domain')): 177 | if not kerberos.check_keychain(read_pref('principal')): 178 | _update_login() 179 | if kerberos.tickets() == []: 180 | NSLog('Could not find a valid Kerberos ticket. Requesting new ticket now...') 181 | success = kerberos.kinit_keychain_command(read_pref('principal')) 182 | result = ('New Kerberos ticket granted!' if success 183 | else 'Unable to get new Kerberos ticket') 184 | notify(result, '') 185 | NSLog(result) 186 | else: 187 | NSLog('Kerberos ticket exists in cache. Attempting to renew ticket...') 188 | if not kerberos.refresh_ticket(): 189 | success = kerberos.kinit_keychain_command(read_pref('principal')) 190 | else: 191 | success = True 192 | result = ('Successfully renewed Kerberos ticket!' if success 193 | else 'Unable to renew Kerberos ticket!') 194 | notify(result, '') 195 | NSLog(result) 196 | kerberos.delete_expired_tickets() 197 | 198 | 199 | def _get_base_args(self, server_url, username): 200 | protocol_map = { 201 | 'http': 'http', 202 | 'https': 'htps', 203 | 'smb': 'smb ', 204 | 'afp': 'afp ', 205 | 'cifs': 'cifs' 206 | } 207 | parsed_url = urlparse.urlparse(server_url) 208 | args = [ 209 | '-l', parsed_url.netloc, 210 | '-a', username, 211 | '-s', parsed_url.netloc, 212 | '-p', parsed_url.path, 213 | '-r', protocol_map[parsed_url.scheme], 214 | ] 215 | return args 216 | 217 | 218 | def check_keychain(self, server_url, username, return_code=True): 219 | args = self._get_base_args(server_url, username) 220 | if keychain('find', 'internet', args): 221 | return True 222 | else: 223 | return False 224 | 225 | 226 | def add_share_to_keychain(self, server_url, username, password): 227 | args = self._get_base_args(server_url, username) + [ 228 | '-w', password, 229 | '-T', '/usr/bin/security', 230 | '-T', '/System/Library/CoreServices/NetAuthAgent.app/Contents/MacOS/NetAuthSysAgent', 231 | '-T', '/System/Library/CoreServices/NetAuthAgent.app/', 232 | '-T', 'group://NetAuth', 233 | '-D', 'Network Password', 234 | ] 235 | output = keychain('add', 'internet', args) 236 | if output == None: 237 | NSLog('Failed to add keychain entry') 238 | return False 239 | else: 240 | NSLog('Successfully added keychain entry') 241 | return True 242 | 243 | 244 | def delete_share_from_keychain(self, server_url, username): 245 | args = self._get_base_args(server_url, username) 246 | if keychain('delete', 'internet', args): 247 | return True 248 | else: 249 | return False 250 | 251 | 252 | def load_prefs(self): 253 | NSLog('Loading user preferences...') 254 | defaults = { 255 | 'managed_shares': list(), 256 | 'user_added_shares': list(), 257 | 'display_notifications': True, 258 | 'group_membership': list(), 259 | 'domain': '', 260 | 'principal': '' 261 | } 262 | for key, value in defaults.iteritems(): 263 | if not read_pref(key): 264 | write_pref(key, value) 265 | if ad.bound(): 266 | write_pref('domain', ad.domain_dns()) 267 | write_pref('principal', ad.principal()) 268 | 269 | 270 | def get_sharebykey(self, key, value): 271 | managed_shares = get_managed_shares() 272 | user_added_shares = get_user_added_shares() 273 | for network_share in managed_shares: 274 | if network_share[key] == value: 275 | return network_share 276 | for network_share in user_added_shares: 277 | if network_share[key] == value: 278 | return network_share 279 | 280 | 281 | def get_managedshare_bykey(self, key, value): 282 | managed_shares = get_managed_shares() 283 | for index, network_share in enumerate(managed_shares): 284 | if network_share[key] == value: 285 | return network_share, index 286 | else: 287 | continue 288 | return None, None 289 | 290 | 291 | def remove_share(self, network_share): 292 | current_share = self.get_sharebykey('title', network_share.get('title')) 293 | user_added_shares = get_user_added_shares() 294 | managed_shares = get_managed_shares() 295 | if current_share: 296 | if current_share.get('share_type') == 'managed': 297 | managed_shares.remove(current_share) 298 | write_pref('managed_shares', managed_shares) 299 | else: 300 | user_added_shares.remove(current_share) 301 | write_pref('user_added_shares', user_added_shares) 302 | 303 | 304 | def get_useradded_bykey(self, key, value): 305 | user_added_shares = get_user_added_shares() 306 | for index, network_share in enumerate(user_added_shares): 307 | if network_share[key] == value: 308 | return network_share, index 309 | return None, None 310 | 311 | 312 | def _process_networkshare(self, network_share, share_type='managed', hide=False, auto_connect=False, username=''): 313 | processed_share = { 314 | 'mount_point': '/Volumes/{0}'.format(os.path.basename(network_share['share_url'])), 315 | 'connect_automatically': auto_connect, 316 | 'hide_from_menu': hide, 317 | 'share_type': share_type, 318 | 'title': network_share['title'], 319 | 'share_url': network_share['share_url'] 320 | } 321 | if network_share.get('groups'): 322 | processed_share['groups'] = network_share.get('groups') 323 | if share_type == 'user': 324 | processed_share['username'] = username 325 | return processed_share 326 | 327 | 328 | def get_mappedshares(self, membership): 329 | NSLog('Looking for network_shares preference...') 330 | if read_pref('network_shares'): 331 | mapped_shares = [network_share 332 | for network_share in read_pref('network_shares') 333 | for group in membership 334 | if group in network_share['groups']] 335 | NSLog('Loaded mapped shares!') 336 | else: 337 | mapped_shares = list() 338 | NSLog('Unable to load mapped shares!') 339 | return mapped_shares 340 | 341 | 342 | def update_managedshares(self): 343 | NSLog('Updating managed shares...') 344 | membership = ad.membership(read_pref('principal')) 345 | managed_shares = get_managed_shares() 346 | mapped_shares = self.get_mappedshares(membership) 347 | mapped_share_titles = [share['title'] for share in mapped_shares] 348 | for mapped_share in mapped_shares: 349 | existing_share, index = self.get_managedshare_bykey('title', mapped_share['title']) 350 | if existing_share: 351 | NSLog('Updating existing share') 352 | if existing_share['share_url'] != mapped_share['share_url']: 353 | managed_shares[index]['share_url'] = mapped_share['share_url'] 354 | if existing_share['groups'] != mapped_share['groups']: 355 | managed_shares[index]['groups'] = mapped_share['groups'] 356 | else: 357 | NSLog('Processing new network share: {0}'.format(mapped_share.get('title'))) 358 | processed_share = self._process_networkshare(mapped_share) 359 | managed_shares.append(processed_share) 360 | write_pref('managed_shares', managed_shares) 361 | 362 | if read_pref('include_smb_home'): 363 | NSLog('Getting SMB Home info...') 364 | existing, index = self.get_managedshare_bykey('share_type', 'smb_home') 365 | if ad.bound(): 366 | smbhome = ad.smbhome() 367 | username = ad._get_consoleuser() 368 | if existing: 369 | NSLog('SMB Home already exists in config. Updating...') 370 | if existing.get('title') != username: 371 | managed_shares[index]['share_title'] = username 372 | if existing.get('share_url') != smbhome: 373 | managed_shares[index]['share_url'] = smbhome 374 | else: 375 | network_share = {'title': username, 'share_url': smbhome} 376 | processed = self._process_networkshare(network_share, 377 | share_type='smb_home') 378 | managed_shares.append(processed) 379 | NSLog('Done checking for SMB Info...') 380 | else: 381 | NSLog('Computer is not bound. Skipping SMB Home...') 382 | write_pref('managed_shares', managed_shares) 383 | 384 | current_shares = list(managed_shares) 385 | for network_share in current_shares: 386 | if (network_share.get('title') not in mapped_share_titles 387 | and network_share.get('share_type') != 'smb_home'): 388 | remove_share(network_share) 389 | NSLog('Managed shares have been updated!') 390 | 391 | 392 | def _process_membership(self, group_membership): 393 | mapped_shares = [network_share 394 | for network_share in read_pref('network_shares') 395 | for group in group_membership 396 | if group in network_share['groups']] 397 | return mapped_shares 398 | 399 | 400 | def add_or_update_usershare(self, title, url, hide, auto_connect, username=''): 401 | network_share = {'title': title, 'share_url': url} 402 | existing_share, index = self.get_useradded_bykey('title', title) 403 | processed_share = self._process_networkshare(network_share, hide=hide, 404 | auto_connect=auto_connect, 405 | share_type='user_added', 406 | username=username) 407 | user_added_shares = get_user_added_shares() 408 | if existing_share: 409 | user_added_shares[index] = processed_share 410 | else: 411 | user_added_shares.append(processed_share) 412 | write_pref('user_added_shares', user_added_shares) 413 | 414 | 415 | def add_or_update_managedshare(self, title, url, hide, auto_connect, username=''): 416 | network_share = {'title': title, 'share_url': url} 417 | existing_share, index = self.get_managedshare_bykey('title', title) 418 | processed_share = self._process_networkshare(network_share, hide=hide, 419 | auto_connect=auto_connect, 420 | share_type='managed') 421 | managed_shares = get_managed_shares() 422 | if existing_share: 423 | managed_shares[index] = processed_share 424 | else: 425 | managed_shares.append(processed_share) 426 | write_pref('user_added_shares', user_added_shares) 427 | 428 | 429 | def update_share(self, modified_share, index): 430 | if modified_share.get('share_type') in ['managed', 'smb_home']: 431 | managed_shares = get_managed_shares() 432 | managed_shares[index] = modified_share 433 | write_pref('managed_shares', managed_shares) 434 | if modified_share.get('share_type') == 'user_added_share': 435 | user_added_shares = get_user_added_shares() 436 | user_added_shares[index] = modified_share 437 | write_pref('user_added_shares', user_added_shares) 438 | 439 | 440 | # borrowed from Imagr and modified for this app 441 | class CustomThread(threading.Thread): 442 | '''Class for running a process in its own thread''' 443 | 444 | def __init__(self, url=None, unmount=None, mountpoint=None): 445 | threading.Thread.__init__(self) 446 | if url: 447 | self.url = url.replace(' ', '%20') 448 | else: 449 | self.url = url 450 | self.unmount = unmount 451 | 452 | 453 | def run(self): 454 | try: 455 | if self.url: 456 | NSLog('Attempting to mount {0}'.format(self.url)) 457 | mount_location = mount_shares_better.mount_share(self.url, show_ui=True) 458 | message = 'Successfully mounted {0}'.format(self.url) 459 | NSLog(message) 460 | notify(message, mount_location) 461 | elif self.unmount: 462 | NSLog('Attempting to unmount {0}'.format(self.unmount)) 463 | _unmount_share_cmd(self.unmount) 464 | message = 'Successfully unmounted {0}'.format(self.unmount) 465 | NSLog(message) 466 | notify('Network share no longer available', message) 467 | except Exception as e: 468 | 469 | if self.url: 470 | message = 'There was a problem mounting share {0}'.format(self.url.replace('%20', ' ')) 471 | alert = PyDialog.AlertDialog('Could not mount share!', message) 472 | alert.display() 473 | NSLog(message) 474 | if self.unmount: 475 | message = 'There was a problem unmounting {0}'.format(self.unmount) 476 | alert = PyDialog.AlertDialog('Something went wrong!', message) 477 | alert.display() 478 | NSLog(message) 479 | pass 480 | -------------------------------------------------------------------------------- /ShareMounter/StatusBarController.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # StatusBarController.py 4 | # ShareMounter 5 | # 6 | # Created by Mr. Kyle Crawshaw on 9/27/15. 7 | # Copyright (c) 2015 Kyle Crawshaw. All rights reserved. 8 | # 9 | 10 | from Foundation import * 11 | from AppKit import * 12 | from SystemConfiguration import * 13 | import objc 14 | import os 15 | import SMUtilities 16 | from pymacad import * 17 | import PyDialog 18 | import CoreFoundation 19 | import subprocess 20 | 21 | 22 | class StatusBarController(NSObject): 23 | '''Main controller object for UI features''' 24 | mainMenu = None 25 | connectMenu = None 26 | statusBar = None 27 | config = None 28 | user_config = None 29 | ldap_reachable = False 30 | shareURLField = objc.IBOutlet() 31 | shareTitleField = objc.IBOutlet() 32 | connectAutoCheck = objc.IBOutlet() 33 | hideFromMenuCheck = objc.IBOutlet() 34 | addShareWindow = objc.IBOutlet() 35 | manageSharesView = objc.IBOutlet() 36 | networkSharesDropdown = objc.IBOutlet() 37 | addNewButton = objc.IBOutlet() 38 | removeButton = objc.IBOutlet() 39 | doneButton = objc.IBOutlet() 40 | # useSSOCheckbox = objc.IBOutlet() 41 | # loginButton = objc.IBOutlet() 42 | 43 | passwordPanel = objc.IBOutlet() 44 | passwordFieldLogin = objc.IBOutlet() 45 | passwordPanelView = objc.IBOutlet() 46 | usernameFieldLogin = objc.IBOutlet() 47 | serverNameLabel = objc.IBOutlet() 48 | 49 | menu_is_updating = False 50 | config_manager = SMUtilities.ConfigManager() 51 | 52 | 53 | def runStartup(self): 54 | self.statusBar = NSStatusBar.systemStatusBar().statusItemWithLength_(-1.0) 55 | statusBarImage = NSImage.imageNamed_('DefaultStatusBarIcon') 56 | statusBarImage.setTemplate_(True) 57 | self.statusBar.button().setImage_(statusBarImage) 58 | self.buildMainMenu() 59 | self.updateConfig() 60 | self.ldap_reachable = SMUtilities.is_ldap_reachable(SMUtilities.read_pref('domain')) 61 | self.registerForWorkspaceNotifications() 62 | self.detect_network_changes() 63 | 64 | 65 | def updateConfig(self): 66 | self.config_manager.validate_kerberos() 67 | self.ldap_reachable = SMUtilities.is_ldap_reachable(SMUtilities.read_pref('domain')) 68 | if self.ldap_reachable: 69 | self.config_manager.update_managedshares() 70 | self.buildConnectMenu() 71 | SMUtilities.notify('Connect menu has been updated!', '') 72 | 73 | 74 | @objc.IBAction 75 | def manualUpdate_(self, sender): 76 | self.ldap_reachable = SMUtilities.is_ldap_reachable(SMUtilities.read_pref('domain')) 77 | if self.ldap_reachable: 78 | self.updateConfig() 79 | else: 80 | d = PyDialog.AlertDialog('Unable to update Connect menu!', 'Domain cannot be reached...') 81 | d.display() 82 | 83 | 84 | @objc.IBAction 85 | def quit_(self, sender): 86 | # self.config_manager.save_prefs() 87 | app = NSApplication.sharedApplication() 88 | app.terminate_(objc.nil) 89 | 90 | 91 | def buildMainMenu(self): 92 | self.mainMenu = NSMenu.alloc().init() 93 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Loading...', None, '').setTarget_(self) 94 | self.mainMenu.addItem_(NSMenuItem.separatorItem()) 95 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Manage Network Shares', self.manageNetworkShares_, '').setTarget_(self) 96 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Ticket Viewer', self.openTicketViewer_, '').setTarget_(self) 97 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Refresh Kerberos', self.refreshKerberosTicket_, '').setTarget_(self) 98 | self.mainMenu.addItem_(NSMenuItem.separatorItem()) 99 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Show Shares on Desktop', self.toggleShowDrivesOnDesktop_, '').setTarget_(self) 100 | if CoreFoundation.CFPreferencesCopyAppValue('ShowMountedServersOnDesktop', 'com.apple.finder'): 101 | self.mainMenu.itemWithTitle_('Show Shares on Desktop').setState_(True) 102 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Display Notifications', self.toggleNotifications_, '').setTarget_(self) 103 | if SMUtilities.read_pref('display_notifications'): 104 | self.mainMenu.itemWithTitle_('Display Notifications').setState_(True) 105 | self.mainMenu.addItem_(NSMenuItem.separatorItem()) 106 | self.mainMenu.addItemWithTitle_action_keyEquivalent_('Quit', self.quit_, '').setTarget_(self) 107 | self.statusBar.setMenu_(self.mainMenu) 108 | 109 | 110 | @objc.IBAction 111 | def openTicketViewer_(self, sender): 112 | app_path = '/System/Library/CoreServices/Ticket Viewer.app' 113 | NSWorkspace.sharedWorkspace().launchApplication_('Ticket Viewer') 114 | 115 | 116 | @objc.IBAction 117 | def refreshKerberosTicket_(self, sender): 118 | self.ldap_reachable = SMUtilities.is_ldap_reachable(SMUtilities.read_pref('domain')) 119 | if self.ldap_reachable: 120 | self.config_manager.validate_kerberos() 121 | else: 122 | d = PyDialog.AlertDialog('Unable to refresh Kerberos Ticket!', 'Domain cannot be reached...') 123 | d.display() 124 | 125 | 126 | def releaseStatusBar(self): 127 | self.mainMenu.release() 128 | NSStatusBar.systemStatusBar().removeStatusItem_(self.statusBar) 129 | self.statusBar.release() 130 | 131 | 132 | def buildShareMenu(self, share): 133 | user_added_shares = SMUtilities.get_user_added_shares() 134 | shareMenu = NSMenu.alloc().init() 135 | connectMenuItem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(share.get('title'), None, '') 136 | if share.get('mount_point') in SMUtilities.get_mounted_network_volumes(): 137 | connectMenuItem.setState_(True) 138 | shareMenu.addItemWithTitle_action_keyEquivalent_('Unmount Share', 139 | self.unmountShare_, 140 | '').setTarget_(self) 141 | shareMenu.addItemWithTitle_action_keyEquivalent_('Open Folder', 142 | self.openFolderClicked_, 143 | '').setTarget_(self) 144 | else: 145 | connectMenuItem.setState_(False) 146 | shareMenu.addItemWithTitle_action_keyEquivalent_('Mount Share', 147 | self.connectToShare_, 148 | '').setTarget_(self) 149 | shareMenu.addItem_(NSMenuItem.separatorItem()) 150 | shareMenu.addItemWithTitle_action_keyEquivalent_('Connect Automatically', 151 | self.toggleAutoConnect_, 152 | '').setTarget_(self) 153 | if share.get('connect_automatically'): 154 | shareMenu.itemWithTitle_('Connect Automatically').setState_(True) 155 | shareMenu.addItemWithTitle_action_keyEquivalent_('Hide from Menu', 156 | self.toggleHideShare_, 157 | '').setTarget_(self) 158 | if share in user_added_shares: 159 | shareMenu.addItemWithTitle_action_keyEquivalent_('Remove from Menu', 160 | self.removeUserShare_, 161 | '').setTarget_(self) 162 | connectMenuItem.setSubmenu_(shareMenu) 163 | return connectMenuItem 164 | 165 | 166 | def updateShareMenu(self, share_path): 167 | mounted_volumes_base_paths = [os.path.basename(mounted) for mounted in SMUtilities.get_mounted_network_volumes()] 168 | share_menu_titles = [menu_item.title() for menu_item in self.connectMenu.itemArray()] 169 | network_share = self.config_manager.get_sharebykey('mount_point', share_path) 170 | if self.ldap_reachable: 171 | NSLog('Updating menu for "{0}"'.format(network_share.get('title'))) 172 | item_index = self.connectMenu.indexOfItemWithTitle_(network_share.get('title')) 173 | self.connectMenu.removeItemAtIndex_(item_index) 174 | self.connectMenu.insertItem_atIndex_(self.buildShareMenu(network_share), item_index) 175 | # below updates the Umount All menu item 176 | for share in share_menu_titles: 177 | if share in mounted_volumes_base_paths: 178 | unmount_all = True 179 | break 180 | else: 181 | unmount_all = False 182 | unmount = self.connectMenu.itemWithTitle_('Unmount All') 183 | if unmount_all: 184 | unmount.setAction_(self.unmountShare_) 185 | unmount.setTarget_(self) 186 | else: 187 | unmount.setAction_(None) 188 | unmount.setTarget_(self) 189 | 190 | 191 | def processManagedShares(self): 192 | managed_shares = SMUtilities.get_managed_shares() 193 | if managed_shares == []: 194 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('No available shares...', None, '') 195 | else: 196 | for share in managed_shares: 197 | share_menu = self.buildShareMenu(share) 198 | self.connectMenu.addItem_(share_menu) 199 | if share.get('hide_from_menu'): 200 | share_menu.setHidden_(True) 201 | if self.ldap_reachable: 202 | hide_all = False 203 | cannot_locate = self.connectMenu.itemWithTitle_('Cannot locate domain') 204 | if cannot_locate: 205 | cannot_locate.setHidden_(True) 206 | else: 207 | NSLog('Could not locate servers added to menu') 208 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('Cannot locate domain', None, '') 209 | hide_all = True 210 | 211 | 212 | def processUserAddedShares(self): 213 | user_added_shares = SMUtilities.get_user_added_shares() 214 | self.connectMenu.addItem_(NSMenuItem.separatorItem()) 215 | if user_added_shares: 216 | no_available_shares = self.connectMenu.indexOfItemWithTitle_('No available shares...') 217 | if no_available_shares != -1: 218 | self.connectMenu.removeItemAtIndex_(no_available_shares) 219 | for share in user_added_shares: 220 | share_menu = self.buildShareMenu(share) 221 | self.connectMenu.addItem_(share_menu) 222 | if share.get('hide_from_menu'): 223 | share_menu.setHidden_(True) 224 | self.connectMenu.addItem_(NSMenuItem.separatorItem()) 225 | if self.ldap_reachable: 226 | hide_all = False 227 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('Show Hidden', self.toggleShowHidden_, '').setTarget_(self) 228 | self.connectMenu.itemWithTitle_('Show Hidden').setHidden_(True) 229 | self.toggleShowHiddenButton() 230 | else: 231 | hide_all = True 232 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('Check For Updates', self.manualUpdate_, '').setTarget_(self) 233 | for shareMenu in self.connectMenu.itemArray(): 234 | if shareMenu.submenu() and hide_all: 235 | shareMenu.setHidden_(True) 236 | 237 | self.connectMenu.addItem_(NSMenuItem.separatorItem()) 238 | if SMUtilities.get_mounted_network_volumes(): 239 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('Unmount All', self.unmountShare_, '').setTarget_(self) 240 | else: 241 | self.connectMenu.addItemWithTitle_action_keyEquivalent_('Unmount All', None, '') 242 | 243 | 244 | def buildConnectMenu(self): 245 | self.menu_is_updating = True 246 | NSLog('building connectMenu') 247 | self.connectMenu = NSMenu.alloc().init() 248 | self.processManagedShares() 249 | self.processUserAddedShares() 250 | self.autoMountShares() 251 | if self.mainMenu.itemAtIndex_(0).title() == 'Loading...': 252 | self.mainMenu.removeItemAtIndex_(0) 253 | self.mainMenu.insertItemWithTitle_action_keyEquivalent_atIndex_('Connect', None, '', 0) 254 | self.mainMenu.itemWithTitle_('Connect').setSubmenu_(self.connectMenu) 255 | self.menu_is_updating = False 256 | NSLog('Connect menu has been updated.') 257 | 258 | 259 | def autoMountShares(self): 260 | managed_shares = SMUtilities.get_managed_shares() 261 | user_added_shares = SMUtilities.get_user_added_shares() 262 | if self.ldap_reachable: 263 | for share in managed_shares: 264 | if (share.get('connect_automatically') 265 | and share.get('mount_point') 266 | not in SMUtilities.get_mounted_network_volumes()): 267 | 268 | NSLog('Automounting {0}'.format(share.get('share_url'))) 269 | SMUtilities.mount_share(share.get('share_url')) 270 | for share in user_added_shares: 271 | if (share.get('connect_automatically') 272 | and share.get('mount_point') 273 | not in SMUtilities.get_mounted_network_volumes()): 274 | SMUtilities.mount_share(share.get('share_url')) 275 | 276 | 277 | def toggleShowHiddenButton(self): 278 | for connectMenuItem in self.connectMenu.itemArray(): 279 | if connectMenuItem.isHidden() and not (connectMenuItem.title() == 'Show Hidden' or connectMenuItem.title() == 'Unmount All'): 280 | hide_menu_item = False 281 | break 282 | else: 283 | hide_menu_item = True 284 | if hide_menu_item and not self.connectMenu.itemWithTitle_('Show Hidden').state(): 285 | self.connectMenu.itemWithTitle_('Show Hidden').setHidden_(True) 286 | else: 287 | self.connectMenu.itemWithTitle_('Show Hidden').setHidden_(False) 288 | 289 | 290 | @objc.IBAction 291 | def connectToShare_(self, sender): 292 | NSLog('User clicked {}'.format(sender.title())) 293 | try: 294 | share_title = sender.parentItem().title() 295 | except AttributeError: 296 | share_title = self.shareTitleField.stringValue() 297 | network_share = self.config_manager.get_sharebykey('title', share_title) 298 | if network_share.get('mount_point') in SMUtilities.get_mounted_network_volumes(): 299 | d = PyDialog.AlertDialog('Cannot mount "{0}"'.format(network_share.get('title')), 300 | 'Volume is already mounted at "{0}"'.format(network_share.get('mount_point'))) 301 | d.display() 302 | else: 303 | SMUtilities.mount_share(network_share.get('share_url')) 304 | 305 | 306 | def getAvailableShares(self): 307 | available_shares = list() 308 | managed_shares = SMUtilities.get_managed_shares() 309 | user_added_shares = SMUtilities.get_user_added_shares() 310 | for share in managed_shares: 311 | share = dict(share) 312 | share['share_type'] = 'managed' 313 | available_shares.append(share) 314 | for share in user_added_shares: 315 | share = dict(share) 316 | share['share_type'] = 'user' 317 | available_shares.append(share) 318 | return available_shares 319 | 320 | 321 | @objc.IBAction 322 | def closePasswordPanel_(self, sender): 323 | self.passwordPanel.orderOut_(self) 324 | 325 | 326 | @objc.IBAction 327 | def manageNetworkShares_(self, sender): 328 | NSLog('User clicked {0}'.format(sender.title())) 329 | # setup positioning of window to be below statusbaritem 330 | status_button = self.statusBar.valueForKey_('window').frame() 331 | x = status_button.origin.x - self.addShareWindow.frame().size.width/2 332 | y = status_button.origin.y 333 | self.addShareWindow.setFrameOrigin_((x,y)) 334 | # make application front and display window 335 | NSApplication.sharedApplication().activateIgnoringOtherApps_(objc.YES) 336 | self.addShareWindow.makeKeyAndOrderFront_(self) 337 | available_shares = self.getAvailableShares() 338 | self.networkSharesDropdown.removeAllItems() 339 | # if available_shares: 340 | # network_share_titles = [share.get('title') for share in available_shares] 341 | # else: 342 | # network_share_titles = list() 343 | # print 'SETTING UP MANAGED SHARES WINDOW' 344 | # self.networkSharesDropdown.removeAllItems() 345 | # self.networkSharesDropdown.addItemsWithTitles_(network_share_titles) 346 | self.setupManageShareWindow(refresh_shares=True) 347 | 348 | 349 | @objc.IBAction 350 | def changeVisibleShare_(self, sender): 351 | self.setupManageShareWindow() 352 | # 353 | # @objc.IBAction 354 | # def useSSOToggled_(self, sender): 355 | # if self.useSSOCheckbox.state(): 356 | # self.loginButton.setHidden_(True) 357 | # else: 358 | # self.loginButton.setHidden_(False) 359 | 360 | 361 | @objc.IBAction 362 | def addNewShareClicked_(self, sender): 363 | self.shareTitleField.setStringValue_('New Share') 364 | self.shareTitleField.setEnabled_(True) 365 | self.shareURLField.setStringValue_('') 366 | self.shareURLField.setEnabled_(True) 367 | # self.useSSOCheckbox.setEnabled_(True) 368 | # self.useSSOCheckbox.setState_(False) 369 | self.hideFromMenuCheck.setState_(False) 370 | self.connectAutoCheck.setState_(False) 371 | self.networkSharesDropdown.addItemsWithTitles_(['New Share']) 372 | self.networkSharesDropdown.selectItemWithTitle_('New Share') 373 | self.removeButton.setHidden_(False) 374 | # self.loginButton.setHidden_(False) 375 | 376 | @objc.IBAction 377 | def saveButtonClicked_(self, sender): 378 | share_url = self.shareURLField.stringValue() 379 | share_title = self.shareTitleField.stringValue() 380 | auto_connect = True if self.connectAutoCheck.state() else False 381 | hide_from_menu = True if self.hideFromMenuCheck.state() else False 382 | selected_item = self.networkSharesDropdown.selectedItem() 383 | # use_kerberos = True if self.useSSOCheckbox.state() else False 384 | 385 | if share_title != selected_item.title(): 386 | index = self.networkSharesDropdown.indexOfSelectedItem() 387 | self.networkSharesDropdown.removeItemAtIndex_(index) 388 | self.networkSharesDropdown.insertItemWithTitle_atIndex_(share_title, index) 389 | self.networkSharesDropdown.selectItemAtIndex_(index) 390 | 391 | share = self.config_manager.get_sharebykey('title', selected_item.title()) 392 | if share: 393 | if share.get('share_type') in ['managed', 'smb_home']: 394 | existing_share, index = self.config_manager.get_managedshare_bykey('title', share.get('title')) 395 | existing_share['connect_automatically'] = auto_connect 396 | existing_share['hide_from_menu'] = hide_from_menu 397 | self.config_manager.update_share(existing_share, index) 398 | else: 399 | existing_share, index = self.config_manager.get_useradded_bykey('title', share.get('title')) 400 | existing_share['connect_automatically'] = auto_connect 401 | existing_share['hide_from_menu'] = hide_from_menu 402 | existing_share['title'] = share_title 403 | existing_share['share_url'] = share_url 404 | self.config_manager.update_share(existing_share, index) 405 | # self.config_manager.user_added_shares[existing_index]['use_kerberos'] = use_kerberos 406 | connect_menu_item = self.connectMenu.itemWithTitle_(selected_item.title()) 407 | connect_menu_item.setTitle_(share_title) 408 | if self.ldap_reachable and not self.connectMenu.itemWithTitle_('Show Hidden').state(): 409 | connect_menu_item.setHidden_(hide_from_menu) 410 | connect_submenu = connect_menu_item.submenu() 411 | connect_submenu.itemWithTitle_('Connect Automatically').setState_(auto_connect) 412 | connect_submenu.itemWithTitle_('Hide from Menu').setState_(hide_from_menu) 413 | else: 414 | self.config_manager.add_or_update_usershare(share_title, share_url, 415 | hide_from_menu, auto_connect) 416 | self.buildConnectMenu() 417 | self.setupManageShareWindow() 418 | 419 | 420 | @objc.IBAction 421 | def removeUserShare_(self, sender): 422 | if sender.title() == 'Remove from Menu': 423 | share, index = self.config_manager.get_useradded_bykey('title', sender.parentItem().title()) 424 | else: 425 | share, index = self.config_manager.get_useradded_bykey('title', self.shareTitleField.stringValue()) 426 | menu_index = self.connectMenu.indexOfItemWithTitle_(share.get('title')) 427 | self.connectMenu.removeItemAtIndex_(menu_index) 428 | self.config_manager.remove_share(share) 429 | first_item = self.networkSharesDropdown.itemArray()[0] 430 | self.networkSharesDropdown.selectItemWithTitle_(first_item.title()) 431 | if self.addShareWindow.isVisible(): 432 | self.setupManageShareWindow(refresh_shares=True) 433 | 434 | 435 | @objc.IBAction 436 | def cancelButtonClicked_(self, sender): 437 | self.addShareWindow.orderOut_(self) 438 | 439 | 440 | def setupManageShareWindow(self, refresh_shares=False): 441 | available_shares = self.getAvailableShares() 442 | if available_shares: 443 | network_share_titles = [share.get('title') for share in available_shares] 444 | if refresh_shares: 445 | self.networkSharesDropdown.removeAllItems() 446 | self.networkSharesDropdown.addItemsWithTitles_(network_share_titles) 447 | 448 | if self.shareTitleField.stringValue() not in network_share_titles: 449 | refresh_shares=True 450 | selected_share_title = self.networkSharesDropdown.titleOfSelectedItem() 451 | 452 | 453 | selected_share = self.config_manager.get_sharebykey('title', selected_share_title) 454 | self.shareURLField.setStringValue_(selected_share.get('share_url')) 455 | self.shareTitleField.setStringValue_(selected_share.get('title')) 456 | if selected_share.get('share_type') in ['managed', 'smb_home']: 457 | self.shareTitleField.setEnabled_(False) 458 | self.shareURLField.setEnabled_(False) 459 | self.removeButton.setHidden_(True) 460 | # self.loginButton.setHidden_(True) 461 | # self.useSSOCheckbox.setEnabled_(False) 462 | # self.useSSOCheckbox.setState_(True) 463 | # self.loginButton.setHidden_(True) 464 | else: 465 | self.shareTitleField.setEnabled_(True) 466 | self.shareURLField.setEnabled_(True) 467 | self.removeButton.setHidden_(False) 468 | # self.useSSOCheckbox.setEnabled_(True) 469 | # if selected_share.get('use_kerberos') == 1: 470 | # self.useSSOCheckbox.setState_(True) 471 | # self.loginButton.setHidden_(True) 472 | # else: 473 | # self.useSSOCheckbox.setState_(False) 474 | # self.loginButton.setHidden_(False) 475 | 476 | self.hideFromMenuCheck.setState_(selected_share.get('hide_from_menu')) 477 | self.connectAutoCheck.setState_(selected_share.get('connect_automatically')) 478 | 479 | 480 | @objc.IBAction 481 | def toggleAutoConnect_(self, sender): 482 | share = self.config_manager.get_sharebykey('title', sender.parentItem().title()) 483 | if share.get('share_type') in ['managed', 'smb_home']: 484 | existing_share, index = self.config_manager.get_managedshare_bykey('title', share.get('title')) 485 | else: 486 | existing_share, index = self.config_manager.get_useradded_bykey('title', share.get('title')) 487 | if existing_share: 488 | if existing_share.get('connect_automatically'): 489 | sender.setState_(False) 490 | existing_share['connect_automatically'] = False 491 | self.config_manager.update_share(existing_share, index) 492 | NSLog('User has set {0} to no longer connect automatically'.format(existing_share.get('title'))) 493 | else: 494 | sender.setState_(True) 495 | existing_share['connect_automatically'] = True 496 | if existing_share.get('mount_point') not in SMUtilities.get_mounted_network_volumes(): 497 | SMUtilities.mount_share(existing_share.get('share_url')) 498 | self.config_manager.update_share(existing_share, index) 499 | NSLog('User has set {0} to connect automatically.'.format(existing_share.get('title'))) 500 | if self.addShareWindow.isVisible(): 501 | self.connectAutoCheck.setState_(share.get('connect_automatically')) 502 | 503 | 504 | @objc.IBAction 505 | def toggleHideShare_(self, sender): 506 | NSLog('User clicked "{0}"'.format(sender.title())) 507 | share = self.config_manager.get_sharebykey('title', sender.parentItem().title()) 508 | if share.get('share_type') in ['managed', 'smb_home']: 509 | existing_share, index = self.config_manager.get_managedshare_bykey('title', share.get('title')) 510 | else: 511 | existing_share, index = self.config_manager.get_useradded_bykey('title', share.get('title')) 512 | if existing_share: 513 | if existing_share.get('hide_from_menu'): 514 | sender.parentItem().setHidden_(False) 515 | sender.setState_(False) 516 | existing_share['hide_from_menu'] = False 517 | self.config_manager.update_share(existing_share, index) 518 | else: 519 | if not self.connectMenu.itemWithTitle_('Show Hidden').state(): 520 | sender.parentItem().setHidden_(True) 521 | else: 522 | sender.setState_(True) 523 | existing_share['hide_from_menu'] = True 524 | self.config_manager.update_share(existing_share, index) 525 | self.toggleShowHiddenButton() 526 | if self.addShareWindow.isVisible(): 527 | self.hideFromMenuCheck.setState_(share.get('hide_from_menu')) 528 | 529 | 530 | @objc.IBAction 531 | def toggleShowHidden_(self, sender): 532 | if sender.state(): 533 | for shareMenu in self.connectMenu.itemArray(): 534 | submenu = shareMenu.submenu() 535 | if submenu: 536 | hidden_flag = submenu.itemWithTitle_('Hide from Menu').state() 537 | if hidden_flag and sender.state(): 538 | shareMenu.setHidden_(True) 539 | sender.setState_(False) 540 | else: 541 | for shareMenu in self.connectMenu.itemArray(): 542 | if shareMenu.isHidden() and sender.state() == False: 543 | shareMenu.setHidden_(False) 544 | shareMenu.submenu().itemWithTitle_('Hide from Menu').setState_(True) 545 | sender.setState_(True) 546 | 547 | 548 | @objc.IBAction 549 | def toggleShowDrivesOnDesktop_(self, sender): 550 | finder = 'com.apple.finder' 551 | message = ''''Finder has to be restarted for changes to take effect. \ 552 | Your desktop will flash for a moment''' 553 | 554 | if CoreFoundation.CFPreferencesCopyAppValue('ShowMountedServersOnDesktop', finder) and sender.state(): 555 | show_icons = False 556 | alert_title = 'Getting ready to hide server icons on desktop.' 557 | else: 558 | show_icons = True 559 | alert_title = 'Getting ready to show server icons on desktop.' 560 | 561 | alert = PyDialog.ContinueDialog(alert_title, message) 562 | alert.display() 563 | if alert.should_continue(): 564 | CoreFoundation.CFPreferencesSetAppValue('ShowMountedServersOnDesktop', show_icons, finder) 565 | sender.setState_(show_icons) 566 | if CoreFoundation.CFPreferencesAppSynchronize(finder): 567 | subprocess.check_output(['/usr/bin/killall', '-HUP', 'Finder']) 568 | 569 | 570 | @objc.IBAction 571 | def toggleNotifications_(self, sender): 572 | if sender.state(): 573 | show_notifications = False 574 | else: 575 | show_notifications = True 576 | sender.setState_(show_notifications) 577 | SMUtilities.write_pref('display_notifications', show_notifications) 578 | 579 | 580 | @objc.IBAction 581 | def openFolderClicked_(self, sender): 582 | share_title = sender.parentItem().title() 583 | share_to_open = self.config_manager.get_sharebykey('title', share_title) 584 | SMUtilities.open_file(share_to_open.get('mount_point')) 585 | 586 | 587 | @objc.IBAction 588 | def unmountShare_(self, sender): 589 | NSLog('User clicked {}'.format(sender.title())) 590 | mounted_volumes = SMUtilities.get_mounted_network_volumes() 591 | if sender.title() == 'Unmount All': 592 | for mounted in mounted_volumes: 593 | SMUtilities.unmount_share(mounted) 594 | else: 595 | network_share = self.config_manager.get_sharebykey('title', sender.parentItem().title()) 596 | if network_share.get('mount_point') in mounted_volumes: 597 | SMUtilities.unmount_share(network_share.get('mount_point')) 598 | 599 | 600 | def unmountAllShares(self): 601 | mounted_volumes = SMUtilities.get_mounted_network_volumes() 602 | for mounted in mounted_volumes: 603 | SMUtilities.unmount_share(mounted) 604 | 605 | 606 | # def userNotificationCenter_shouldPresentNotification_(self, center, notification): 607 | # return objc.YES 608 | 609 | 610 | def registerForWorkspaceNotifications(self): 611 | nc = NSWorkspace.sharedWorkspace().notificationCenter() 612 | notifications = [NSWorkspaceDidMountNotification, 613 | NSWorkspaceDidUnmountNotification] 614 | for n in notifications: 615 | nc.addObserver_selector_name_object_(self, 616 | self.wsNotificationReceived, 617 | n, 618 | None) 619 | NSLog('Registered for Workspace Notifications') 620 | 621 | 622 | def wsNotificationReceived(self, notification): 623 | notification_name = notification.name() 624 | user_info = notification.userInfo() 625 | NSLog("NSWorkspace notification was: %@", notification_name) 626 | if notification_name == NSWorkspaceDidMountNotification: 627 | new_volume = user_info['NSDevicePath'] 628 | NSLog("%@ was mounted", new_volume) 629 | update_volume = new_volume 630 | elif notification_name == NSWorkspaceDidUnmountNotification: 631 | removed_volume = user_info['NSDevicePath'] 632 | NSLog("%@ was unmounted", removed_volume) 633 | update_volume = removed_volume 634 | elif notification_name == NSWorkspaceDidRenameVolumeNotification: 635 | update_volume = None 636 | 637 | network_share = self.config_manager.get_sharebykey('mount_point', update_volume) 638 | if update_volume and network_share: 639 | if self.connectMenu.itemWithTitle_(network_share.get('title')): 640 | self.updateShareMenu(update_volume) 641 | else: 642 | NSLog('Connect Menu is empty. No need to update') 643 | 644 | 645 | def networkStateHasChanged(self, store, keys, info): 646 | NSLog('Network state has changed') 647 | self.ldap_reachable = SMUtilities.is_ldap_reachable(SMUtilities.read_pref('domain')) 648 | NSLog('Menu is updating: {0}'.format(self.menu_is_updating)) 649 | if self.menu_is_updating == False: 650 | self.menu_is_updating = True 651 | NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(3.5, 652 | self, 653 | self.updateConfig, 654 | None, 655 | objc.NO) 656 | 657 | 658 | def detect_network_changes(self): 659 | store = SCDynamicStoreCreate(None, 660 | "global-network-watcher", 661 | self.networkStateHasChanged, None) 662 | SCDynamicStoreSetNotificationKeys(store, 663 | None, 664 | ['State:/Network/Global/IPv4']) 665 | CFRunLoopAddSource(CFRunLoopGetCurrent(), 666 | SCDynamicStoreCreateRunLoopSource(None, store, 0), 667 | kCFRunLoopCommonModes) 668 | CFRunLoopRun() 669 | NSLog('Started monitoring for network state changes...') 670 | -------------------------------------------------------------------------------- /ShareMounter/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /ShareMounter/en.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 608 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 664 | 679 | 688 | 700 | 709 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | NSAllRomanInputSourcesLocaleIdentifier 805 | 806 | 807 | 808 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | -------------------------------------------------------------------------------- /ShareMounter/main.m: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | 5 | int main(int argc, const char * argv[]) { 6 | 7 | @autoreleasepool { 8 | 9 | NSBundle *mainBundle = [NSBundle mainBundle]; 10 | NSString *resourcePath = [mainBundle resourcePath]; 11 | NSArray *pythonPathArray = [NSArray arrayWithObjects: resourcePath, [resourcePath stringByAppendingPathComponent:@"PyObjC"], nil]; 12 | 13 | setenv("PYTHONPATH", [[pythonPathArray componentsJoinedByString:@":"] UTF8String], 1); 14 | 15 | NSArray *possibleMainExtensions = [NSArray arrayWithObjects: @"py", @"pyc", @"pyo", nil]; 16 | NSString *mainFilePath = nil; 17 | 18 | for (NSString *possibleMainExtension in possibleMainExtensions) { 19 | mainFilePath = [mainBundle pathForResource: @"main" ofType: possibleMainExtension]; 20 | if ( mainFilePath != nil ) break; 21 | } 22 | 23 | if ( !mainFilePath ) { 24 | [NSException raise: NSInternalInconsistencyException format: @"%s:%d main() Failed to find the Main.{py,pyc,pyo} file in the application wrapper's Resources directory.", __FILE__, __LINE__]; 25 | } 26 | 27 | Py_SetProgramName("/usr/bin/python"); 28 | Py_Initialize(); 29 | PySys_SetArgv(argc, (char **)argv); 30 | 31 | const char *mainFilePathPtr = [mainFilePath UTF8String]; 32 | FILE *mainFile = fopen(mainFilePathPtr, "r"); 33 | int result = PyRun_SimpleFile(mainFile, (char *)[[mainFilePath lastPathComponent] UTF8String]); 34 | 35 | if ( result != 0 ) { 36 | [NSException raise: NSInternalInconsistencyException 37 | format: @"%s:%d main() PyRun_SimpleFile failed with file '%@'. See console for errors.", __FILE__, __LINE__, mainFilePath]; 38 | } 39 | return result; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ShareMounter/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # main.py 4 | # ShareMounter 5 | # 6 | # Created by Mr. Kyle Crawshaw on 9/27/15. 7 | # Copyright (c) 2015 Kyle Crawshaw. All rights reserved. 8 | # 9 | 10 | # import modules required by application 11 | import objc 12 | import Foundation 13 | import AppKit 14 | 15 | from PyObjCTools import AppHelper 16 | 17 | # import modules containing classes required to start application and load MainMenu.nib 18 | import AppDelegate 19 | import StatusBarController 20 | 21 | # pass control to AppKit 22 | AppHelper.runEventLoop() 23 | -------------------------------------------------------------------------------- /ShareMounter/mount_shares_better.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Credit Michael Lynn, aka Pudquick # 3 | # https://gist.github.com/pudquick/1362a8908be01e23041d # 4 | ############################################################################### 5 | 6 | import objc, CoreFoundation, Foundation 7 | 8 | class attrdict(dict): 9 | __getattr__ = dict.__getitem__ 10 | __setattr__ = dict.__setitem__ 11 | 12 | NetFS = attrdict() 13 | # Can cheat and provide 'None' for the identifier, it'll just use frameworkPath instead 14 | # scan_classes=False means only add the contents of this Framework 15 | NetFS_bundle = objc.initFrameworkWrapper('NetFS', frameworkIdentifier=None, 16 | frameworkPath=objc.pathForFramework('NetFS.framework'), 17 | globals=NetFS, scan_classes=False) 18 | 19 | # https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html 20 | # Fix NetFSMountURLSync signature 21 | del NetFS['NetFSMountURLSync'] 22 | objc.loadBundleFunctions(NetFS_bundle, NetFS, [('NetFSMountURLSync', 'i@@@@@@o^@')]) 23 | 24 | def mount_share(share_path, show_ui=False): 25 | # Mounts a share at /Volumes, returns the mount point or raises an error 26 | sh_url = CoreFoundation.CFURLCreateWithString(None, share_path, None) 27 | if not show_ui: 28 | # Set UI to reduced interaction 29 | open_options = {NetFS.kNAUIOptionKey: NetFS.kNAUIOptionNoUI} 30 | else: 31 | open_options = None 32 | # Allow mounting sub-directories of root shares 33 | mount_options = {NetFS.kNetFSAllowSubMountsKey: True} 34 | # Mount! 35 | result, output = NetFS.NetFSMountURLSync(sh_url, None, None, None, open_options, mount_options, None) 36 | # Check if it worked 37 | if result != 0: 38 | raise Exception('Error mounting url "%s": %s' % (share_path, output)) 39 | # Return the mountpath 40 | return str(output[0]) 41 | 42 | def mount_share_at_path(share_path, mount_path): 43 | # Mounts a share at the specified path, returns the mount point or raises an error 44 | sh_url = CoreFoundation.CFURLCreateWithString(None, share_path, None) 45 | mo_url = CoreFoundation.CFURLCreateWithString(None, mount_path, None) 46 | # Set UI to reduced interaction 47 | open_options = {NetFS.kNAUIOptionKey: NetFS.kNAUIOptionNoUI} 48 | # Allow mounting sub-directories of root shares 49 | # Also specify the share should be mounted directly at (not under) mount_path 50 | mount_options = { 51 | NetFS.kNetFSAllowSubMountsKey: True, 52 | NetFS.kNetFSMountAtMountDirKey: True, 53 | } 54 | # Mount! 55 | result, output = NetFS.NetFSMountURLSync(sh_url, mo_url, None, None, open_options, mount_options, None) 56 | # Check if it worked 57 | if result != 0: 58 | raise Exception('Error mounting url "%s" at path "%s": %s' % (share_path, mount_path, output)) 59 | # Return the mountpath 60 | return str(output[0]) 61 | -------------------------------------------------------------------------------- /ShareMounter/pymacad/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Shufflepuck/pymacad.svg?branch=master)](https://travis-ci.org/Shufflepuck/pymacad) 2 | [![codecov.io](https://codecov.io/github/Shufflepuck/pymacad/coverage.svg?branch=master)](https://codecov.io/github/ftiff/pymacad?branch=master) 3 | 4 | # pymacad 5 | 6 | ## Acknowledgments 7 | 8 | This python package is based on KerbMinder (http://github.com/pmbuko/KerbMinder). 9 | 10 | * [Peter Bukowinski](http://github.com/pmbuko), author of KerbMinder 11 | * [Francois 'ftiff' Levaux-Tiffreau](http://github.com/ftiff), who extracted this package 12 | * [Ben Toms](http://github.com/macmule), who gave ftiff the idea 13 | * [Allister Banks](https://twitter.com/Sacrilicious/status/543451138239258624) for pointing out an effective dig command to test for domain reachability. 14 | * [Kyle Crawshaw](http://github.com/kcrawshaw), who extended it significantly 15 | 16 | ## pymacad.ad 17 | 18 | I would suggest to use `from pymacad import ad` -- then call using ad.xxx 19 | 20 | ### Example 21 | ```python 22 | >>> from pymacad import ad 23 | >>> ad.bound() 24 | False 25 | >>> ad.accessible('TEST.COM') 26 | False 27 | >>> ad.accessible('FTI.IO') 28 | True 29 | ``` 30 | 31 | ### Functions 32 | 33 | #### ad.bound() 34 | checks if computer is bound to AD 35 | - returns True or False 36 | - raises subprocess.CalledProcessError 37 | 38 | #### ad.principal(user) 39 | gets principal from AD. If no user is specified, uses the current user. 40 | - Returns principal 41 | - Raises NotBound, NotReachable or subprocess.CalledProcessError 42 | 43 | #### ad.accessible(domain) 44 | checks if domain can be joined. 45 | - Returns True or False 46 | - raises subprocess.CalledProcessError 47 | 48 | #### ad.searchnodes() 49 | returns a list of available directories 50 | 51 | #### ad.adnode() 52 | returns the first Active Directory node or None 53 | 54 | #### ad.get_domain_dns() 55 | returns the DNS of domain, or raises NotBound 56 | 57 | #### ad.membership(user) 58 | Returns a list of groups belonging to this user 59 | Raises NotBound 60 | 61 | #### ad.realms() 62 | Returns a list of Kerberos realms, or NotBound 63 | 64 | #### ad.smb_home() 65 | Returns the home URL of the user, or an empty string. 66 | 67 | ### Exceptions 68 | - pymacad.ad.NotReachable 69 | - pymacad.ad.NotBound 70 | - subprocess.CalledProcessError 71 | 72 | 73 | ##Kerbereros 74 | 75 | ####kerberos.caches() 76 | Returns a list of cached credentials. Can optionally specify `kerberos.caches(details=True)` to get extra details 77 | 78 | ####kerberos.principal_fromcache() 79 | Returns the principal from and existing cache or returns None if no cache exists 80 | -------------------------------------------------------------------------------- /ShareMounter/pymacad/__init__.py: -------------------------------------------------------------------------------- 1 | from . import kerberos 2 | from . import ad 3 | -------------------------------------------------------------------------------- /ShareMounter/pymacad/ad/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import SystemConfiguration 3 | 4 | def _get_consoleuser(): 5 | return SystemConfiguration.SCDynamicStoreCopyConsoleUser(None, None, None)[0] 6 | 7 | 8 | def _cmd_dig_check(domain): 9 | try: 10 | dig = subprocess.check_output(['dig', '-t', 'srv', '_ldap._tcp.' + domain]) 11 | except subprocess.CalledProcessError: 12 | raise 13 | else: 14 | return dig 15 | 16 | 17 | def _cmd_dsconfigad_show(): 18 | return subprocess.check_output(['dsconfigad', '-show']) 19 | 20 | 21 | def _cmd_dscl(nodename='.', scope=None, query=None, user=_get_consoleuser(), plist=False): 22 | if not scope: 23 | scope = '/Users/{0}'.format(user) 24 | cmd = ['/usr/bin/dscl', nodename, '-read', scope] 25 | if plist: 26 | cmd.insert(1, '-plist') 27 | if query: 28 | cmd.append(query) 29 | try: 30 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 31 | if plist: 32 | import plistlib 33 | return plistlib.readPlistFromString(output) 34 | else: 35 | return output 36 | except subprocess.CalledProcessError: 37 | return None 38 | 39 | 40 | def _cmd_ldapsearch(domain, fields=None): 41 | ldap_url = 'ldap://{0}'.format(domain) 42 | domain_split = domain.split('.') 43 | base = 'dc={0},dc={1}'.format(domain_split[0],domain_split[1]) 44 | cmd = ['ldapsearch', '-LLL', '-Q', '-H', ldap_url, '-b', base] 45 | if fields: 46 | if isinstance(fields, list): 47 | cmd.extend(fields) 48 | else: 49 | cmd.append(fields) 50 | if not accessible(domain): 51 | raise NotReachable 52 | out = subprocess.check_output(cmd) 53 | return out 54 | 55 | 56 | def bound(): 57 | try: 58 | output = _cmd_dsconfigad_show() 59 | 60 | if "Active Directory" in output: 61 | return True 62 | else: 63 | return False 64 | except subprocess.CalledProcessError: 65 | raise 66 | 67 | 68 | def searchnodes(): 69 | if not bound(): 70 | raise NotBound 71 | net_config = SystemConfiguration.SCDynamicStoreCreate(None, 'directory-nodes', None, None) 72 | nodes = SystemConfiguration.SCDynamicStoreCopyValue(net_config, 'com.apple.opendirectoryd.node:/Search') 73 | if nodes: 74 | return list(nodes) 75 | else: 76 | return None 77 | 78 | 79 | def adnode(): 80 | if not bound(): 81 | raise NotBound 82 | nodes = searchnodes() 83 | ad_node = [node for node in nodes if 'Active Directory' in node] 84 | return ad_node[0] if ad_node else None 85 | 86 | 87 | def domain_dns(): 88 | if not bound(): 89 | raise NotBound 90 | net_config = SystemConfiguration.SCDynamicStoreCreate(None, 'active-directory', None, None) 91 | ad_info = SystemConfiguration.SCDynamicStoreCopyValue(net_config, 'com.apple.opendirectoryd.ActiveDirectory') 92 | if ad_info: 93 | return ad_info.get('DomainNameDns') 94 | else: 95 | return None 96 | 97 | 98 | class ProcessError(subprocess.CalledProcessError): 99 | pass 100 | 101 | 102 | class NotReachable(Exception): 103 | '''Domain is unreachable''' 104 | pass 105 | 106 | 107 | class NotBound(Exception): 108 | '''Computer is not bound to a Directory Service''' 109 | pass 110 | 111 | 112 | class PrincipalFormatError(Exception): 113 | '''Principal is formatted incorrectly. Must be "someuser@DOMAIN.COM" 114 | or "someuser@domain.com"''' 115 | pass 116 | 117 | def _extract_principal(string): 118 | import re 119 | try: 120 | match = re.search(r'[a-zA-Z0-9+_\-\.]+@[^;]+\.[A-Z]{2,}', string, re.IGNORECASE) 121 | match = match.group() 122 | except AttributeError: 123 | raise 124 | else: 125 | return match 126 | 127 | 128 | def _format_principal(principal): 129 | try: 130 | p_split = _split_principal(principal) 131 | formatted = '{0}@{1}'.format(p_split[0], p_split[1].upper()) 132 | except IndexError: 133 | raise PrincipalFormatError('Invalid format for principal!') 134 | return formatted 135 | 136 | def _split_principal(principal): 137 | '''Returns user and domain tuple''' 138 | p_split = principal.split('@') 139 | if len(p_split) == 2: 140 | return p_split 141 | else: 142 | raise PrincipalFormatError('Invalid format for principal: {0}'.format(principal)) 143 | 144 | 145 | def principal(user=_get_consoleuser()): 146 | """Returns the principal of the current user when computer is bound""" 147 | 148 | if not bound(): 149 | raise NotBound 150 | 151 | user_path = '/Users/' + user 152 | 153 | try: 154 | output = _cmd_dscl('/Search', query='AuthenticationAuthority', scope=user_path) 155 | if not output: 156 | return None 157 | result = _extract_principal(output) 158 | return result 159 | except AttributeError: 160 | raise NotReachable 161 | except subprocess.CalledProcessError: 162 | raise 163 | else: 164 | return None 165 | 166 | 167 | def accessible(domain=''): 168 | if domain == '': 169 | domain = domain_dns() 170 | try: 171 | dig = _cmd_dig_check(domain) 172 | except subprocess.CalledProcessError: 173 | raise 174 | else: 175 | if 'ANSWER SECTION' not in dig: 176 | return False 177 | else: 178 | return True 179 | 180 | 181 | def membership(principal): 182 | user, domain = _split_principal(principal) 183 | fields = ['sAMAccountName={0}'.format(user), 'memberOf'] 184 | ldap_query = _cmd_ldapsearch(domain, fields=fields) 185 | if ldap_query: 186 | membership = [line[line.find('CN=')+3:line.find(',')] 187 | for line in ldap_query.split('\n') 188 | if 'memberOf' in line] 189 | return membership 190 | else: 191 | return None 192 | 193 | 194 | def realms(): 195 | if not bound(): 196 | raise NotBound 197 | store = SystemConfiguration.SCDynamicStoreCreate(None, 'default-realms', None, None) 198 | realms = SystemConfiguration.SCDynamicStoreCopyValue(store, 'Kerberos-Default-Realms') 199 | return list(realms) if realms else None 200 | 201 | 202 | def smbhome(node='.', user=_get_consoleuser()): 203 | if not bound(): 204 | raise NotBound 205 | output = _cmd_dscl(nodename=node, query='SMBHome', user=user) 206 | if output and 'No such key:' not in output: 207 | out_split = output.split(' ')[1] 208 | smb_home = out_split.replace('\\\\', '/').replace('\\', '/').strip('\n') 209 | smb_url = '{0}{1}'.format('smb:/', smb_home) 210 | return smb_url 211 | else: 212 | return '' 213 | -------------------------------------------------------------------------------- /ShareMounter/pymacad/ad/test_ad.py: -------------------------------------------------------------------------------- 1 | import nose 2 | import mock 3 | from pymacad import ad 4 | import unittest 5 | import subprocess 6 | 7 | 8 | 9 | class TestBound(unittest.TestCase): 10 | _dsconfigad_bound = """ 11 | Active Directory Forest = test.com 12 | Active Directory Domain = test.com 13 | Computer Account = test-machine$ 14 | 15 | Advanced Options - User Experience 16 | Create mobile account at login = Disabled 17 | Require confirmation = Disabled 18 | Force home to startup disk = Enabled 19 | Mount home as sharepoint = Enabled 20 | Use Windows UNC path for home = Enabled 21 | Network protocol to be used = smb 22 | Default user Shell = /bin/bash 23 | 24 | Advanced Options - Mappings 25 | Mapping UID to attribute = not set 26 | Mapping user GID to attribute = not set 27 | Mapping group GID to attribute = not set 28 | Generate Kerberos authority = Enabled 29 | 30 | Advanced Options - Administrative 31 | Preferred Domain controller = not set 32 | Allowed admin groups = TEST\STAFF 33 | Authentication from any domain = Enabled 34 | Packet signing = allow 35 | Packet encryption = allow 36 | Password change interval = 14 37 | Restrict Dynamic DNS updates = not set 38 | Namespace mode = domain 39 | """ 40 | _dsconfigad_notbound = "" 41 | _side_effect=subprocess.CalledProcessError(1, ['dsconfigad', '-show'], output="") 42 | 43 | @mock.patch('pymacad.ad._cmd_dsconfigad_show', mock.Mock(return_value=_dsconfigad_notbound)) 44 | def test_bound_false(self): 45 | self.assertFalse(ad.bound()) 46 | 47 | @mock.patch('pymacad.ad._cmd_dsconfigad_show', mock.Mock(return_value=_dsconfigad_bound)) 48 | def test_bound_true(self): 49 | nose.tools.ok_(ad.bound()) 50 | 51 | @mock.patch('pymacad.ad._cmd_dsconfigad_show', mock.Mock(side_effect=_side_effect)) 52 | def test_bound_exception(self): 53 | nose.tools.assert_raises(subprocess.CalledProcessError, ad.bound) 54 | 55 | 56 | class TestPrincipal(unittest.TestCase): 57 | 58 | _dscl_search_notreachable= """ 59 | AuthenticationAuthority: ;ShadowHash;HASHLIST: ;Kerberosv5;;testuser@LKDC:SHA1.8392019230AABB3399494BB1191999AAAF999AA;LKDC:SHA1.8392019230AABB3399494BB1191999AAAF999AA ;Kerberosv5Cert;;9170197410974109731097BBAA10101001029CC@LKDC:SHA1.9170197410974109731097BBAA10101001029CC;LKDC:SHA1.9170197410974109731097BBAA10101001029CC; 60 | """ 61 | 62 | _dscl_search_reachable = _dscl_search_notreachable + """ 63 | AuthenticationAuthority: ;Kerberosv5;;testuser@TEST.COM;TEST.COM; ;NetLogon;testuser;TEST 64 | No such key: AuthenticationAuthority 65 | """ 66 | 67 | @mock.patch('pymacad.ad.bound', mock.Mock(return_value=False)) 68 | def test_principal_notbound(self): 69 | nose.tools.assert_raises(ad.NotBound, ad.principal) 70 | 71 | @mock.patch('pymacad.ad.bound', mock.Mock(return_value=True)) 72 | @mock.patch('pymacad.ad._cmd_dscl_search', mock.Mock(return_value=_dscl_search_reachable)) 73 | def test_principal_ok(self): 74 | nose.tools.eq_("testuser@TEST.COM", ad.principal('testuser')) 75 | 76 | @mock.patch('pymacad.ad.bound', mock.Mock(return_value=True)) 77 | @mock.patch('pymacad.ad._cmd_dscl_search', mock.Mock(return_value=_dscl_search_reachable)) 78 | @mock.patch('pymacad.ad._extract_principal', mock.Mock(side_effect=AttributeError)) 79 | def test_principal_attribute_error(self): 80 | nose.tools.assert_raises(ad.NotReachable, ad.principal) 81 | 82 | _side_effect=subprocess.CalledProcessError(1, ['dscl', '/Search', 'read', 'testuser', 'AuthenticationAuthority'], output="") 83 | @mock.patch('pymacad.ad.bound', mock.Mock(return_value=True)) 84 | @mock.patch('pymacad.ad._cmd_dscl_search', mock.Mock(return_value=_dscl_search_reachable)) 85 | @mock.patch('pymacad.ad._extract_principal', mock.Mock(side_effect=_side_effect)) 86 | def test_principal_process_error(self): 87 | nose.tools.assert_raises(subprocess.CalledProcessError, ad.principal) 88 | 89 | class TestDig(unittest.TestCase): 90 | _dig_ok = """ 91 | ;; Truncated, retrying in TCP mode. 92 | 93 | ; <<>> DiG 9.8.3-P1 <<>> -t srv _ldap._tcp.test.com 94 | ;; global options: +cmd 95 | ;; Got answer: 96 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 616 97 | ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 2 98 | 99 | ;; QUESTION SECTION: 100 | ;_ldap._tcp.test.com. IN SRV 101 | 102 | ;; ANSWER SECTION: 103 | _ldap._tcp.test.com. 600 IN SRV 0 100 389 ad.test.com. 104 | 105 | ;; AUTHORITY SECTION: 106 | test.com. 1800 IN NS ns1.test.com. 107 | test.com. 1800 IN NS ns2.test.com. 108 | 109 | ;; ADDITIONAL SECTION: 110 | ns1.test.com. 1200 IN A 10.0.0.1 111 | ns2.test.com. 1200 IN A 10.0.0.2 112 | 113 | ;; Query time: 1 msec 114 | ;; SERVER: 10.0.0.2#53(10.0.0.2) 115 | ;; WHEN: Mon Oct 17 11:20:54 2015 116 | """ 117 | 118 | _dig_notok = """ 119 | ; <<>> DiG 9.8.3-P1 <<>> -t srv _ldap._tcp.test.com 120 | ;; global options: +cmd 121 | ;; Got answer: 122 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 696 123 | ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 124 | 125 | ;; QUESTION SECTION: 126 | ;_ldap._tcp.test.com. IN SRV 127 | 128 | ;; AUTHORITY SECTION: 129 | . 10800 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2015101900 1800 900 604800 86400 130 | 131 | ;; Query time: 21 msec 132 | ;; SERVER: 10.0.0.2#53(10.0.0.2) 133 | ;; WHEN: Mon Oct 17 11:20:54 2015 134 | """ 135 | 136 | @mock.patch('pymacad.ad._cmd_dig_check', mock.Mock(return_value=_dig_ok)) 137 | def test_accessible_ok(self): 138 | nose.tools.ok_(ad.accessible('TEST.COM')) 139 | 140 | @mock.patch('pymacad.ad._cmd_dig_check', mock.Mock(return_value=_dig_notok)) 141 | def test_accessible_ok(self): 142 | self.assertFalse(ad.accessible('TEST.COM')) 143 | 144 | _side_effect=subprocess.CalledProcessError(1, ['dig', '-t', 'srv', '_ldap._tcp.TEST.COM'], output="") 145 | @mock.patch('pymacad.ad._cmd_dig_check', mock.Mock(side_effect=_side_effect)) 146 | def test_accessible_processerror(self): 147 | nose.tools.assert_raises(subprocess.CalledProcessError, ad.accessible, 'TEST.COM') 148 | -------------------------------------------------------------------------------- /ShareMounter/pymacad/kerberos/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from .. import ad 3 | 4 | _incorrect_pass_attempts = 0 5 | class KeychainError(Exception): 6 | pass 7 | 8 | def _keychain(action_type, item_type, args, return_code=False): 9 | import os 10 | available_actions = ['add', 'find', 'delete'] 11 | if action_type not in available_actions: 12 | raise AttributeError('action_type must be in {0}'.format(available_actions)) 13 | available_types = ['generic', 'internet'] 14 | if item_type not in available_types: 15 | raise AttributeError('item_type must be in {0}'.format(available_types)) 16 | 17 | action = '{0}-{1}-password'.format(action_type, item_type) 18 | user_keychain = os.path.expanduser('~/Library/Keychains/login.keychain') 19 | 20 | if isinstance(args, str): 21 | args = list(args) 22 | 23 | cmd = ['/usr/bin/security', action] + args + [user_keychain] 24 | if return_code: 25 | return subprocess.call(cmd) 26 | else: 27 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, 28 | stderr=subprocess.PIPE) 29 | out, err = p.communicate() 30 | if err: 31 | if 'exists' in err: 32 | raise KeychainError('Keychain item already exists') 33 | elif 'could not be found' in err: 34 | return False 35 | else: 36 | print err 37 | else: 38 | return True 39 | 40 | 41 | def _cmd_klist(flag=None): 42 | import json 43 | cmd = ['/usr/bin/klist', '--json'] 44 | if flag: 45 | cmd.append(flag) 46 | klist_output = subprocess.check_output(cmd) 47 | return json.loads(klist_output) 48 | 49 | 50 | def check_keychain(principal=''): 51 | if principal == '': 52 | principal = ad.principal() 53 | username, realm = ad._split_principal(principal) 54 | security_args = [ 55 | '-a', username, 56 | '-l', realm.upper() + ' (' + username + ')', 57 | '-s', realm.upper(), 58 | '-c', 'aapl' 59 | ] 60 | return True if _keychain('find', 'generic', security_args) else False 61 | 62 | 63 | def pass_to_keychain(principal, password): 64 | """Saves password to keychain for use by kinit.""" 65 | username, realm = ad._split_principal(principal) 66 | security_args = [ 67 | '-a', username, 68 | '-l', realm + '(' + username + ')', 69 | '-s', realm, 70 | '-c', 'aapl', 71 | '-T', '/usr/bin/kinit', 72 | '-w', str(password) 73 | ] 74 | return _keychain('add', 'generic', security_args) 75 | 76 | 77 | def test_kerberos_password(principal, password): 78 | """Runs the kinit command with supplied password.""" 79 | renew1 = subprocess.Popen(['echo', password], stdout=subprocess.PIPE) 80 | renew2 = subprocess.Popen(['kinit','-l','10h','--renewable', 81 | '--password-file=STDIN','--keychain', 82 | ad._format_principal(principal)], 83 | stderr=subprocess.PIPE, 84 | stdin=renew1.stdout, 85 | stdout=subprocess.PIPE) 86 | renew1.stdout.close() 87 | 88 | out = renew2.communicate()[1] 89 | if 'incorrect' in out or 'unknown' in out: 90 | return False 91 | elif out == '': 92 | return True 93 | elif 'revoked' in out: 94 | return 'Account Disabled' 95 | else: 96 | return out 97 | 98 | 99 | def kinit_keychain_command(principal): 100 | """Runs the kinit command with keychain password.""" 101 | if not check_keychain(principal): 102 | return False 103 | cmd = ['/usr/bin/kinit', '-l', '10h', '--renewable', 104 | '--keychain', ad._format_principal(principal)] 105 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 106 | out, err = p.communicate() 107 | if err and 'unable to reach' in err: 108 | raise ad.NotReachable('Unable to get new kerberos ticket. Domain unreachable.') 109 | else: 110 | return True 111 | 112 | 113 | def refresh_ticket(): 114 | try: 115 | subprocess.check_output(['/usr/bin/kinit', '--renew'], 116 | stderr=subprocess.STDOUT) 117 | return True 118 | except subprocess.CalledProcessError: 119 | return False 120 | 121 | 122 | def principal_fromcache(): 123 | '''Returns the principal from cache. This works for bound 124 | and unbound computers. Bound machines should use principal()''' 125 | cache = _cmd_klist() 126 | return cache.get('principal') 127 | 128 | 129 | def destroy(credential=None, cache=None, principal=None): 130 | '''Wrapper around kdestroy. Deletes all kerberos tickets if no values are 131 | provided.''' 132 | if credential: 133 | flag = '--credential={0}'.format(ad._format_principal(credential)) 134 | elif cache: 135 | flag = '--cache={0}'.format(cache) 136 | elif principal: 137 | flag = '--principal={0}'.format(ad._format_principal(principal)) 138 | else: 139 | flag = '--all' 140 | cmd = ['/usr/bin/kdestroy', flag] 141 | try: 142 | out = subprocess.check_output(cmd) 143 | return True 144 | except subprocess.CalledProcessError: 145 | return False 146 | 147 | 148 | def tickets(details=False): 149 | caches = _cmd_klist('--all-content') 150 | if caches: 151 | if details: 152 | tickets = caches.get('tickets') 153 | else: 154 | tickets = list() 155 | for cache in caches.get('tickets'): 156 | for ticket in cache.get('tickets'): 157 | tickets.append(ticket) 158 | return tickets 159 | else: 160 | return None 161 | 162 | 163 | def caches(details=False): 164 | out = _cmd_klist('--list-all') 165 | if out: 166 | if details: 167 | caches = out 168 | else: 169 | caches = [cache.get('Cache name') for cache in out] 170 | return caches 171 | else: 172 | return list() 173 | 174 | 175 | def delete_expired_caches(): 176 | current_caches = caches(details=True) 177 | for c in current_caches: 178 | if c.get('Expired') == 'yes': 179 | destroy(cache=c.get('Cache name')) 180 | 181 | 182 | def delete_expired_tickets(): 183 | for t in tickets(details=True): 184 | if len(t.get('tickets')) == 0: 185 | destroy(cache=t.get('cache')) 186 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **NOTE: THIS PROJECT IS UNDER HEAVY DEVELOPMENT AND SHOULD NOT BE USED IN PRODUCTION YET.** 2 | 3 | *** 4 | 5 | ***Usage*** 6 | 7 | [Download the latest release]('https://github.com/kylecrawshaw/ShareMounter/releases') of ShareMounter. The application will run standalone, but System Admins can also provide either a plist or mobileconfig profile with a list of network shares. ShareMounter is `defaults` aware and relies on `CFPreferences` to keep both managed and user preferences in sync. 8 | 9 | ShareMounter can be run and installed on a Mac regardless of whether it is bound to Active Directory or not. 10 | 11 | 12 | ***About*** 13 | 14 | The goal of ShareMounter is to automatically map and mount network file shares for users based on Active Directory group membership. ShareMounter is a menu bar app that users can customize to their liking. 15 | 16 | When the application is first launched it will check whether or not the domain is reachable. If it is, ShareMounter will get the group membership for the currently logged in user and compare group membership to the admin provided servers found in `/Library/Preferences/ShareMounter.plist` (example below). After determining available shares for the current user the menu will be displayed with the list of shares. ShareMounter will then start to watch for network state changes. 17 | 18 | - If the domain cannot be reached all shares will be hidden. 19 | - If a network share is set to `connect_automatically` it will be automatically mounted on launch or on network state change. 20 | - When a user first launches the application their preferences and available shares are saved to `~/Library/Preferences/ShareMounter.plist` 21 | 22 | You can optionally launch this application as a LaunchAgent so that it will be run when each user logs in. 23 | 24 | 25 | Feedback and pull requests are welcome! 26 | 27 | ***Requirements*** 28 | - System Admins should supply a list of network shares with titles and allowed AD groups 29 | - `/Library/Preferences/ShareMounter.plist` 30 | - `ShareMounter.mobileconfig` 31 | - Required keys for each share: 32 | - `share_url` 33 | - `title` 34 | - `groups` 35 | - Optional keys: 36 | - `hide_from_menu` 37 | - `connect_automatically` 38 | 39 | 40 | ***Example mobileconfig profile*** 41 | This example profile was generated using [MCXToProfile]('https://github.com/timsutton/mcxToProfile') 42 | ``` 43 | 44 | 45 | 46 | 47 | PayloadContent 48 | 49 | 50 | PayloadContent 51 | 52 | ShareMounter 53 | 54 | Forced 55 | 56 | 57 | mcx_preference_settings 58 | 59 | include_smb_home 60 | 61 | network_shares 62 | 63 | 64 | groups 65 | 66 | Domain Admins 67 | IT 68 | 69 | share_url 70 | smb://server1.example.com/Share1 71 | title 72 | Share1 73 | 74 | 75 | groups 76 | 77 | Domain Admins 78 | DevelopmentOffice 79 | 80 | share_url 81 | smb://server2.example.com/Development 82 | title 83 | Development 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | PayloadEnabled 92 | 93 | PayloadIdentifier 94 | MCXToProfile.704db073-425f-49be-8a4f-82b821a5b1ab.alacarte.customsettings.e10452e0-10f3-4e26-8d07-ad030d430cf9 95 | PayloadType 96 | com.apple.ManagedClient.preferences 97 | PayloadUUID 98 | e10452e0-10f3-4e26-8d07-ad030d430cf9 99 | PayloadVersion 100 | 1 101 | 102 | 103 | PayloadDescription 104 | Included custom settings: 105 | ShareMounter 106 | 107 | Git revision: a14a19d7f0 108 | PayloadDisplayName 109 | ShareMounter Managed Preferences 110 | PayloadIdentifier 111 | ShareMounter 112 | PayloadOrganization 113 | 114 | PayloadRemovalDisallowed 115 | 116 | PayloadScope 117 | System 118 | PayloadType 119 | Configuration 120 | PayloadUUID 121 | 704db073-425f-49be-8a4f-82b821a5b1ab 122 | PayloadVersion 123 | 1 124 | 125 | 126 | ``` 127 | 128 | ***Example plist*** 129 | ``` 130 | 131 | 132 | 133 | 134 | include_smb_home 135 | 136 | network_shares 137 | 138 | 139 | groups 140 | 141 | Domain Admins 142 | IT 143 | 144 | share_url 145 | smb://server1.example.com/Share1 146 | title 147 | Share1 148 | 149 | 150 | groups 151 | 152 | Domain Admins 153 | DevelopmentOffice 154 | 155 | share_url 156 | smb://server2.example.com/Development 157 | title 158 | Development 159 | 160 | 161 | 162 | 163 | ``` 164 | 165 | ***Credits*** 166 | 167 | ShareMounter was inspired by a number of projects from these fine individuals 168 | - Michael Lynn (aka pudquick, frogor, mikeymikey) -- mount_shares_better.py 169 | - Graham Gilbert -- Imagr 170 | - Peter Bukowinski -- KerbMinder 171 | - Ben Toms (aka Macmule) -- Many blogposts and scripts 172 | 173 | ***Problems?*** 174 | 175 | Open an issue or better yet, submit a pull request with the fix. I want to make this as versatile of a tool as possible. 176 | --------------------------------------------------------------------------------