diff --git a/ToggleProxy.py b/ToggleProxy.py index 250706e..25f73a4 100644 --- a/ToggleProxy.py +++ b/ToggleProxy.py @@ -1,34 +1,84 @@ #!/usr/bin/env python -from Foundation import NSLog, kCFRunLoopCommonModes, kCFAllocatorDefault, CFDictionaryGetValue, CFRunLoopAddSource -from AppKit import NSObject, NSImage, NSStatusBar, NSVariableStatusItemLength, NSMenu, NSMenuItem, NSRunLoop, NSOnState, NSApp, NSLog, NSOffState, NSApplication -from SystemConfiguration import kSCNetworkProtocolTypeProxies, kSCPropNetProxiesFTPEnable, kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesRTSPEnable, kSCPropNetProxiesSOCKSEnable, kSCNetworkProtocolTypeProxies -from SystemConfiguration import SCDynamicStoreCreate, SCNetworkServiceCopyProtocol, SCNetworkProtocolGetConfiguration, SCDynamicStoreCopyValue, SCPreferencesCreate, SCNetworkServiceCopyAll, SCNetworkServiceGetInterface, SCNetworkInterfaceGetBSDName, SCDynamicStoreSetNotificationKeys, SCDynamicStoreCreateRunLoopSource, SCDynamicStoreCopyProxies, SCNetworkServiceGetName -import commands, re +from Foundation import NSLog, kCFRunLoopCommonModes, kCFAllocatorDefault, CFDictionaryGetValue, CFRunLoopAddSource, NSUserDefaults +from AppKit import NSObject, NSImage, NSStatusBar, NSVariableStatusItemLength, NSMenu, NSMenuItem, NSRunLoop, NSOnState, NSApp, NSLog, NSOffState, NSApplication +from SystemConfiguration import kSCNetworkProtocolTypeProxies, kSCPropNetProxiesFTPEnable, kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesRTSPEnable, kSCPropNetProxiesSOCKSEnable, kSCPropNetProxiesFTPProxy, kSCPropNetProxiesHTTPProxy, kSCPropNetProxiesHTTPSProxy, kSCPropNetProxiesRTSPProxy, kSCPropNetProxiesSOCKSProxy, kSCPropNetProxiesFTPPort, \ + kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesRTSPPort, kSCPropNetProxiesSOCKSPort, kSCNetworkProtocolTypeProxies +from SystemConfiguration import SCDynamicStoreCreate, SCNetworkServiceCopyProtocol, SCNetworkProtocolGetConfiguration, SCDynamicStoreCopyValue, SCPreferencesCreate, SCNetworkServiceCopyAll, SCNetworkServiceGetInterface, SCNetworkInterfaceGetBSDName, SCDynamicStoreSetNotificationKeys, SCDynamicStoreCreateRunLoopSource, SCDynamicStoreCopyProxies, SCNetworkServiceGetName +import commands, re -class ToggleProxy(NSObject): +class ToggleProxy(NSObject): # This is a dictionary of the proxy-types we support, each with a # dictionary of some unique attributes for each, namely: # - # 'pref' : This is a constant defining which preference itemmarks if this proxy is enabled + # 'prefEnable' : This is a constant defining which preference item marks if this proxy is enabled + # 'prefProxy' : This is a constant defining which preference item holds the proxy host + # 'prefPort' : This is a constant defining which preference item holds the proxy port # 'title' : This is what will appear in the menu # 'action' : This is the method that will be called if the user toggles this proxies menuitem - # 'keyEquivalent' : Self-explanatory, but unused + # 'keyEquivalent' : Self-explanatory # 'menuitem' : This will store the menu item for this proxy once it is created - - proxies = { - 'ftp' : { 'pref': kSCPropNetProxiesFTPEnable, 'title': 'FTP Proxy', 'action': 'toggleFtpProxy:', 'keyEquivalent': "", 'menuitem': None }, - 'http' : { 'pref': kSCPropNetProxiesHTTPEnable, 'title': 'HTTP Proxy', 'action': 'toggleHttpProxy:', 'keyEquivalent': "", 'menuitem': None }, - 'https': { 'pref': kSCPropNetProxiesHTTPSEnable, 'title': 'HTTPS Proxy', 'action': 'toggleHttpsProxy:', 'keyEquivalent': "", 'menuitem': None }, - 'rtsp' : { 'pref': kSCPropNetProxiesRTSPEnable, 'title': 'RTSP Proxy', 'action': 'toggleRtspProxy:', 'keyEquivalent': "", 'menuitem': None }, - 'socks': { 'pref': kSCPropNetProxiesSOCKSEnable, 'title': 'SOCKS Proxy', 'action': 'toggleSocksProxy:', 'keyEquivalent': "", 'menuitem': None }, + # 'envVariable' : Environment variable to set with system proxy settings, if needed + + proxyTypes = { + 'http': {'prefEnable': kSCPropNetProxiesHTTPEnable, + 'prefProxy': kSCPropNetProxiesHTTPProxy, + 'prefPort': kSCPropNetProxiesHTTPPort, + 'title': 'HTTP Proxy', + 'action': 'toggleHttpProxy:', + 'keyEquivalent': "", + 'menuitem': None, + 'envVariable': {"http_proxy", "HTTP_PROXY"}}, + 'https': {'prefEnable': kSCPropNetProxiesHTTPSEnable, + 'prefProxy': kSCPropNetProxiesHTTPSProxy, + 'prefPort': kSCPropNetProxiesHTTPSPort, + 'title': 'HTTPS Proxy', 'action': 'toggleHttpsProxy:', + 'keyEquivalent': "", + 'menuitem': None, + 'envVariable': {"https_proxy", "HTTPS_PROXY"}}, + 'ftp': {'prefEnable': kSCPropNetProxiesFTPEnable, + 'prefProxy': kSCPropNetProxiesFTPProxy, + 'prefPort': kSCPropNetProxiesFTPPort, + 'title': 'FTP Proxy', + 'action': 'toggleFtpProxy:', + 'keyEquivalent': "", + 'menuitem': None, + 'envVariable': {"ftp_proxy", "FTP_PROXY"}}, + 'rtsp': {'prefEnable': kSCPropNetProxiesRTSPEnable, + 'prefProxy': kSCPropNetProxiesRTSPProxy, + 'prefPort': kSCPropNetProxiesRTSPPort, + 'title': 'RTSP Proxy', + 'action': 'toggleRtspProxy:', + 'keyEquivalent': "", + 'menuitem': None, + 'envVariable': None}, + 'socks': {'prefEnable': kSCPropNetProxiesSOCKSEnable, + 'prefProxy': kSCPropNetProxiesSOCKSProxy, + 'prefPort': kSCPropNetProxiesSOCKSPort, + 'title': 'SOCKS Proxy', + 'action': 'toggleSocksProxy:', + 'keyEquivalent': "", + 'menuitem': None, + 'envVariable': None}, } + def log(self,content): + if(self.shouldLog): + NSLog(content); + def applicationDidFinishLaunching_(self, notification): + # Toggle logging from Terminal: + # $ defaults write name.klep.toggleproxy logging -bool YES/NO + self.shouldLog = NSUserDefaults.standardUserDefaults().boolForKey_("logging") + # load icon files - self.active_image = NSImage.imageNamed_("active") - self.inactive_image = NSImage.imageNamed_("inactive") + self.active_image = NSImage.imageNamed_("StatusBarImage") + self.inactive_image = NSImage.imageNamed_("StatusBarImage-inactive") + self.no_network_image = NSImage.imageNamed_("StatusBarImage-noNetwork") + self.active_image.setTemplate_(True) + self.inactive_image.setTemplate_(True) + self.no_network_image.setTemplate_(True) # make status bar item self.statusitem = NSStatusBar.systemStatusBar().statusItemWithLength_(NSVariableStatusItemLength) @@ -42,38 +92,20 @@ def applicationDidFinishLaunching_(self, notification): # open connection to the dynamic (configuration) store self.store = SCDynamicStoreCreate(None, "name.klep.toggleproxy", self.dynamicStoreCallback, None) + self.prefDict = SCNetworkProtocolGetConfiguration(SCNetworkServiceCopyProtocol(self.service, kSCNetworkProtocolTypeProxies)) + self.constructMenu() - proxyRef = SCNetworkServiceCopyProtocol(self.service, kSCNetworkProtocolTypeProxies) - prefDict = SCNetworkProtocolGetConfiguration(proxyRef) + self.watchForProxyOrIpChanges() + self.updateUI() + self.setEnvVariables() - separatorRequired = False - # For each of the proxies we are concerned with, check to see if any - # are configured. If so (even if not enabled), create a menuitem for - # that proxy type. - for proxy in self.proxies.values(): - enabled = CFDictionaryGetValue(prefDict, proxy['pref']) - if enabled is not None: - proxy['menuitem'] = self.menu.addItemWithTitle_action_keyEquivalent_( - proxy['title'], - proxy['action'], - proxy['keyEquivalent'] - ) - separatorRequired = True - else: - proxy['menuitem'] = None - if separatorRequired: - self.menu.addItem_(NSMenuItem.separatorItem()) - - # Need a way to quit - self.menu.addItemWithTitle_action_keyEquivalent_("Quit", "quitApp:", "") - # Start working - # self.loadNetworkServices() - self.watchForProxyChanges() - self.updateProxyStatus() + @property + def is_ip_assigned(self): + return SCDynamicStoreCopyValue(self.store, 'State:/Network/Global/IPv4') is not None @property def interface(self): @@ -92,73 +124,136 @@ def service(self): return serviceRef return None - def watchForProxyChanges(self): - """ install a watcher for proxy changes """ - SCDynamicStoreSetNotificationKeys(self.store, None, [ 'State:/Network/Global/Proxies' ]) + def constructMenu(self): + self.menu.removeAllItems() + + separator_required = False + if self.is_ip_assigned: + # For each of the proxyTypes we are concerned with, check to see if any + # are configured. If so (even if not enabled), create a menuitem for + # that proxy type. + for proxy in self.proxyTypes.values(): + enabled = CFDictionaryGetValue(self.prefDict, proxy['prefEnable']) + if enabled is not None: + proxy['menuitem'] = self.menu.addItemWithTitle_action_keyEquivalent_( + proxy['title'], + proxy['action'], + proxy['keyEquivalent'] + ) + separator_required = True + else: + proxy['menuitem'] = None + else: + self.menu.addItemWithTitle_action_keyEquivalent_("No connection - Please connect to any network before using this tool", None, "") + + if separator_required: + self.menu.addItem_(NSMenuItem.separatorItem()) + + # Need a way to quit + self.menu.addItemWithTitle_action_keyEquivalent_("Quit", "quitApp:", "q") - source = SCDynamicStoreCreateRunLoopSource(None, self.store, 0) - loop = NSRunLoop.currentRunLoop().getCFRunLoop() + + def watchForProxyOrIpChanges(self): + """ install a watcher for proxy and Ip changes """ + SCDynamicStoreSetNotificationKeys(self.store, None, ['State:/Network/Global/Proxies', 'State:/Network/Global/IPv4']) + source = SCDynamicStoreCreateRunLoopSource(None, self.store, 0) + loop = NSRunLoop.currentRunLoop().getCFRunLoop() CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) def dynamicStoreCallback(self, store, keys, info): """ callback for watcher """ - self.updateProxyStatus() - - def updateProxyStatus(self): - """ update proxy status """ - # load proxy dictionary - proxydict = SCDynamicStoreCopyProxies(None) - - # get status for primary interface - status = proxydict['__SCOPED__'][self.interface] - - # Are any proxies active now? - anyProxyEnabled = False - - # update menu items according to their related proxy state - for proxy in self.proxies.values(): - if proxy['menuitem']: - proxy['menuitem'].setState_(status.get(proxy['pref'], False) and NSOnState or NSOffState) - if status.get(proxy['pref'], False): - anyProxyEnabled = True - - # set image - self.statusitem.setImage_(anyProxyEnabled and self.active_image or self.inactive_image) + self.log("Proxy or IP change detected") + + # could be an interface change, we have to rebuild menu from scratch in case the proxy configuration is different + self.constructMenu() + self.updateUI() + self.setEnvVariables() + + def updateUI(self): + if self.is_ip_assigned: + self.log("Update proxy status on menu items") + # load proxy dictionary + proxydict = SCDynamicStoreCopyProxies(None) + + # get status for primary interface + status = proxydict['__SCOPED__'][self.interface] + + # Are any proxies active now? + anyProxyEnabled = False + + # update menu items according to their related proxy state + for proxy in self.proxyTypes.values(): + if proxy['menuitem']: + proxy['menuitem'].setState_(status.get(proxy['prefEnable'], False) and NSOnState or NSOffState) + if status.get(proxy['prefEnable'], False): + anyProxyEnabled = True + + # set image + self.statusitem.setImage_(anyProxyEnabled and self.active_image or self.inactive_image) + else: + self.statusitem.setImage_(self.no_network_image) + + def setEnvVariables(self): + if self.is_ip_assigned: + self.log("Setting env var according to system settings") + # load proxy dictionary + proxydict = SCDynamicStoreCopyProxies(None) + # get status for primary interface + status = proxydict['__SCOPED__'][self.interface] + # update menu items according to their related proxy state + for proxy in self.proxyTypes.values(): + if proxy['menuitem'] and proxy['envVariable']: + if status.get(proxy['prefEnable'], False): + for envvar in proxy['envVariable']: + self.executeCommand("launchctl setenv %s '%s'" % (envvar, "http://"+CFDictionaryGetValue(self.prefDict, proxy['prefProxy']) + ":" + str(CFDictionaryGetValue(self.prefDict, proxy['prefPort'])))) + else: + for envvar in proxy['envVariable']: + self.executeCommand("launchctl unsetenv %s" % envvar) def quitApp_(self, sender): NSApp.terminate_(self) def toggleFtpProxy_(self, sender): - self.toggleProxy(self.proxies['ftp']['menuitem'], 'ftpproxy') + self.toggleProxy(self.proxyTypes['ftp']['menuitem'], 'ftpproxy') def toggleHttpProxy_(self, sender): - self.toggleProxy(self.proxies['http']['menuitem'], 'webproxy') + self.toggleProxy(self.proxyTypes['http']['menuitem'], 'webproxy') def toggleHttpsProxy_(self, sender): - self.toggleProxy(self.proxies['https']['menuitem'], 'securewebproxy') + self.toggleProxy(self.proxyTypes['https']['menuitem'], 'securewebproxy') def toggleRtspProxy_(self, sender): - self.toggleProxy(self.proxies['rtsp']['menuitem'], 'streamingproxy') + self.toggleProxy(self.proxyTypes['rtsp']['menuitem'], 'streamingproxy') def toggleSocksProxy_(self, sender): - self.toggleProxy(self.proxies['socks']['menuitem'], 'socksfirewallproxy') + self.toggleProxy(self.proxyTypes['socks']['menuitem'], 'socksfirewallproxy') + + def executeCommand(self, command): + self.log("[Exec Command] %s" % command) + commands.getoutput(command) def toggleProxy(self, item, target): """ callback for clicks on menu item """ servicename = SCNetworkServiceGetName(self.service) if not servicename: - NSLog("interface '%s' not found in services?" % self.interface) + self.log("interface '%s' not found in services?" % self.interface) return newstate = item.state() == NSOffState and 'on' or 'off' - commands.getoutput("/usr/sbin/networksetup -set%sstate '%s' %s" % ( + self.executeCommand("/usr/sbin/networksetup -set%sstate '%s' %s" % ( target, servicename, newstate )) - self.updateProxyStatus() + + # sometimes the UI will be updated to fast if we don't wait a little (resulting in wrongly enabled proxies in the menu) + self.executeCommand("sleep 1") + self.updateUI() + self.setEnvVariables() + + if __name__ == '__main__': - sharedapp = NSApplication.sharedApplication() - toggler = ToggleProxy.alloc().init() + sharedapp = NSApplication.sharedApplication() + toggler = ToggleProxy.alloc().init() sharedapp.setDelegate_(toggler) sharedapp.run() diff --git a/resources/StatusBarImage-inactive@2x.png b/resources/StatusBarImage-inactive@2x.png new file mode 100644 index 0000000..5c1f243 Binary files /dev/null and b/resources/StatusBarImage-inactive@2x.png differ diff --git a/resources/StatusBarImage-noNetwork@2x.png b/resources/StatusBarImage-noNetwork@2x.png new file mode 100644 index 0000000..5ea7896 Binary files /dev/null and b/resources/StatusBarImage-noNetwork@2x.png differ diff --git a/resources/StatusBarImage@2x.png b/resources/StatusBarImage@2x.png new file mode 100644 index 0000000..18bc3c1 Binary files /dev/null and b/resources/StatusBarImage@2x.png differ diff --git a/resources/active.png b/resources/active.png deleted file mode 100644 index b31be71..0000000 Binary files a/resources/active.png and /dev/null differ diff --git a/resources/icon-0-0-0.png b/resources/icon-0-0-0.png deleted file mode 100644 index 3563fea..0000000 Binary files a/resources/icon-0-0-0.png and /dev/null differ diff --git a/resources/icon-0-0-1.png b/resources/icon-0-0-1.png deleted file mode 100644 index 71e3a3f..0000000 Binary files a/resources/icon-0-0-1.png and /dev/null differ diff --git a/resources/icon-0-1-0.png b/resources/icon-0-1-0.png deleted file mode 100644 index e5fc4a1..0000000 Binary files a/resources/icon-0-1-0.png and /dev/null differ diff --git a/resources/icon-0-1-1.png b/resources/icon-0-1-1.png deleted file mode 100644 index d4637cf..0000000 Binary files a/resources/icon-0-1-1.png and /dev/null differ diff --git a/resources/icon-1-0-0.png b/resources/icon-1-0-0.png deleted file mode 100644 index d8c8445..0000000 Binary files a/resources/icon-1-0-0.png and /dev/null differ diff --git a/resources/icon-1-0-1.png b/resources/icon-1-0-1.png deleted file mode 100644 index 4240a06..0000000 Binary files a/resources/icon-1-0-1.png and /dev/null differ diff --git a/resources/icon-1-1-0.png b/resources/icon-1-1-0.png deleted file mode 100644 index d6fc99b..0000000 Binary files a/resources/icon-1-1-0.png and /dev/null differ diff --git a/resources/icon-1-1-1.png b/resources/icon-1-1-1.png deleted file mode 100644 index 4636536..0000000 Binary files a/resources/icon-1-1-1.png and /dev/null differ diff --git a/resources/inactive.png b/resources/inactive.png deleted file mode 100644 index d8db76c..0000000 Binary files a/resources/inactive.png and /dev/null differ diff --git a/resources/menuicon.psd b/resources/menuicon.psd deleted file mode 100644 index cab72f7..0000000 Binary files a/resources/menuicon.psd and /dev/null differ diff --git a/setup.py b/setup.py index 2404963..6ac01e2 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ data_files = glob('resources/*.png'), options = dict(py2app = dict( plist = dict( - LSBackgroundOnly = True + CFBundleIdentifier = 'name.klep.toggleproxy', + LSBackgroundOnly = True, ) )) )