├── .gitignore ├── .hgignore ├── README.md ├── ToggleProxy.py ├── resources ├── StatusBarImage-inactive@2x.png ├── StatusBarImage-noNetwork@2x.png └── StatusBarImage@2x.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | build 3 | dist 4 | test* 5 | *.sw? 6 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | dist/* 4 | build/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ToggleProxy 2 | 3 | _ToggleProxy_ is a [PyObjC][pyobjc]-based application for Mac OS X to 4 | quickly enable and disable the different types of proxy (HTTP, HTTPS, 5 | SOCKS) for the currently active networking interface. 6 | 7 | Clicking the icon (an upward arrow, shown in the Mac OS X menu bar) 8 | presents a menu where the state of each type of proxy can be toggled. 9 | 10 | *NB*: this is not a proxy manager, you have to configure proxies from Mac 11 | OS X Network preferences first before this app makes any sense. 12 | 13 | ## Download 14 | 15 | You can download a pre-built version from 16 | [the Releases page](https://github.com/robertklep/ToggleProxy/releases). 17 | 18 | ## Prerequisites 19 | 20 | * PyObjC: this should be already installed on your Mac, possible after 21 | installing [XCode][xcode] first; 22 | * [py2app][py2app] 23 | 24 | [xcode]: http://itunes.apple.com/us/app/xcode/id448457090?mt=12 25 | [py2app]: https://bitbucket.org/ronaldoussoren/py2app 26 | 27 | ## Build 28 | 29 | Build the application from Terminal like so: 30 | 31 | python setup.py py2app 32 | 33 | This builds the application as `dist/ToggleProxy.app`. You can run it from 34 | there, or move it to somewhere more appropriate first. 35 | 36 | [pyobjc]: http://pyobjc.sourceforge.net/ 37 | 38 | ## Troubleshooting 39 | 40 | If you run into problems, first try to find out what the reason is. Run 41 | this from the same directory as `setup.py`: 42 | 43 | ./dist/ToggleProxy.app/Contents/MacOS/ToggleProxy 44 | 45 | This should output any errors to stdout, so you might get a clue as to why 46 | it's not working. If it warrants an issue report, you're welcome! 47 | 48 | ## Environment 49 | 50 | _ToggleProxy_ will call `launchctl` to set various environment variables to point to the current proxy value: 51 | 52 | - `http_proxy/HTTP_PROXY` 53 | - `https_proxy/HTTPS_PROXY` 54 | - `ftp_proxy/FTP_PROXY` 55 | 56 | To retrieve the current value, you can call this command (from the commandline, or a shell script): 57 | 58 | launchctl getenv http_proxy 59 | 60 | ## Authors / Contributors 61 | 62 | - @robertklep 63 | - @arnaudruffin 64 | - @mkoistinen 65 | 66 | ## Author & License 67 | 68 | Copyright (C) 2011-2013 by Robert Klep (_robert AT klep DOT name_). 69 | 70 | Permission is hereby granted, free of charge, to any person 71 | obtaining a copy of this software and associated documentation 72 | files (the "Software"), to deal in the Software without 73 | restriction, including without limitation the rights to use, 74 | copy, modify, merge, publish, distribute, sublicense, and/or 75 | sell copies of the Software, and to permit persons to whom the 76 | Software is furnished to do so, subject to the following 77 | conditions: 78 | 79 | The above copyright notice and this permission notice shall be 80 | included in all copies or substantial portions of the Software. 81 | 82 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 83 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 84 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 85 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 86 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 87 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 88 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 89 | OTHER DEALINGS IN THE SOFTWARE. 90 | -------------------------------------------------------------------------------- /ToggleProxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from Foundation import NSLog, kCFRunLoopCommonModes, kCFAllocatorDefault, CFDictionaryGetValue, CFRunLoopAddSource, NSUserDefaults 4 | from AppKit import NSObject, NSImage, NSStatusBar, NSVariableStatusItemLength, NSMenu, NSMenuItem, NSRunLoop, NSOnState, NSApp, NSLog, NSOffState, NSApplication 5 | from SystemConfiguration import kSCNetworkProtocolTypeProxies, kSCPropNetProxiesFTPEnable, kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesRTSPEnable, kSCPropNetProxiesSOCKSEnable, kSCPropNetProxiesFTPProxy, kSCPropNetProxiesHTTPProxy, kSCPropNetProxiesHTTPSProxy, kSCPropNetProxiesRTSPProxy, kSCPropNetProxiesSOCKSProxy, kSCPropNetProxiesFTPPort, \ 6 | kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesRTSPPort, kSCPropNetProxiesSOCKSPort, kSCNetworkProtocolTypeProxies 7 | from SystemConfiguration import SCDynamicStoreCreate, SCNetworkServiceCopyProtocol, SCNetworkProtocolGetConfiguration, SCDynamicStoreCopyValue, SCPreferencesCreate, SCNetworkServiceCopyAll, SCNetworkServiceGetInterface, SCNetworkInterfaceGetBSDName, SCDynamicStoreSetNotificationKeys, SCDynamicStoreCreateRunLoopSource, SCDynamicStoreCopyProxies, SCNetworkServiceGetName 8 | import commands, re 9 | 10 | class ToggleProxy(NSObject): 11 | # This is a dictionary of the proxy-types we support, each with a 12 | # dictionary of some unique attributes for each, namely: 13 | # 14 | # 'prefEnable' : This is a constant defining which preference item marks if this proxy is enabled 15 | # 'prefProxy' : This is a constant defining which preference item holds the proxy host 16 | # 'prefPort' : This is a constant defining which preference item holds the proxy port 17 | # 'title' : This is what will appear in the menu 18 | # 'action' : This is the method that will be called if the user toggles this proxies menuitem 19 | # 'keyEquivalent' : Self-explanatory 20 | # 'menuitem' : This will store the menu item for this proxy once it is created 21 | # 'envVariable' : Environment variable to set with system proxy settings, if needed 22 | 23 | proxyTypes = { 24 | 'http': {'prefEnable': kSCPropNetProxiesHTTPEnable, 25 | 'prefProxy': kSCPropNetProxiesHTTPProxy, 26 | 'prefPort': kSCPropNetProxiesHTTPPort, 27 | 'title': 'HTTP Proxy', 28 | 'action': 'toggleHttpProxy:', 29 | 'keyEquivalent': "", 30 | 'menuitem': None, 31 | 'envVariable': {"http_proxy", "HTTP_PROXY"}}, 32 | 'https': {'prefEnable': kSCPropNetProxiesHTTPSEnable, 33 | 'prefProxy': kSCPropNetProxiesHTTPSProxy, 34 | 'prefPort': kSCPropNetProxiesHTTPSPort, 35 | 'title': 'HTTPS Proxy', 'action': 'toggleHttpsProxy:', 36 | 'keyEquivalent': "", 37 | 'menuitem': None, 38 | 'envVariable': {"https_proxy", "HTTPS_PROXY"}}, 39 | 'ftp': {'prefEnable': kSCPropNetProxiesFTPEnable, 40 | 'prefProxy': kSCPropNetProxiesFTPProxy, 41 | 'prefPort': kSCPropNetProxiesFTPPort, 42 | 'title': 'FTP Proxy', 43 | 'action': 'toggleFtpProxy:', 44 | 'keyEquivalent': "", 45 | 'menuitem': None, 46 | 'envVariable': {"ftp_proxy", "FTP_PROXY"}}, 47 | 'rtsp': {'prefEnable': kSCPropNetProxiesRTSPEnable, 48 | 'prefProxy': kSCPropNetProxiesRTSPProxy, 49 | 'prefPort': kSCPropNetProxiesRTSPPort, 50 | 'title': 'RTSP Proxy', 51 | 'action': 'toggleRtspProxy:', 52 | 'keyEquivalent': "", 53 | 'menuitem': None, 54 | 'envVariable': None}, 55 | 'socks': {'prefEnable': kSCPropNetProxiesSOCKSEnable, 56 | 'prefProxy': kSCPropNetProxiesSOCKSProxy, 57 | 'prefPort': kSCPropNetProxiesSOCKSPort, 58 | 'title': 'SOCKS Proxy', 59 | 'action': 'toggleSocksProxy:', 60 | 'keyEquivalent': "", 61 | 'menuitem': None, 62 | 'envVariable': None}, 63 | } 64 | 65 | def log(self, content): 66 | # Toggle logging from Terminal: 67 | # $ defaults write name.klep.toggleproxy logging -bool YES/NO 68 | if NSUserDefaults.standardUserDefaults().boolForKey_("logging"): 69 | NSLog(content) 70 | 71 | def applicationDidFinishLaunching_(self, notification): 72 | # load icon files 73 | self.active_image = NSImage.imageNamed_("StatusBarImage") 74 | self.inactive_image = NSImage.imageNamed_("StatusBarImage-inactive") 75 | self.no_network_image = NSImage.imageNamed_("StatusBarImage-noNetwork") 76 | self.active_image.setTemplate_(True) 77 | self.inactive_image.setTemplate_(True) 78 | self.no_network_image.setTemplate_(True) 79 | 80 | # make status bar item 81 | self.statusitem = NSStatusBar.systemStatusBar().statusItemWithLength_(NSVariableStatusItemLength) 82 | self.statusitem.retain() 83 | self.statusitem.setHighlightMode_(False) 84 | self.statusitem.setEnabled_(True) 85 | 86 | # insert a menu into the status bar item 87 | self.menu = NSMenu.alloc().init() 88 | self.statusitem.setMenu_(self.menu) 89 | 90 | # open connection to the dynamic (configuration) store 91 | self.store = SCDynamicStoreCreate(None, "name.klep.toggleproxy", self.dynamicStoreCallback, None) 92 | self.prefDict = SCNetworkProtocolGetConfiguration(SCNetworkServiceCopyProtocol(self.service, kSCNetworkProtocolTypeProxies)) 93 | self.constructMenu() 94 | 95 | self.watchForProxyOrIpChanges() 96 | self.updateUI() 97 | self.setEnvVariables() 98 | 99 | @property 100 | def is_ip_assigned(self): 101 | return SCDynamicStoreCopyValue(self.store, 'State:/Network/Global/IPv4') is not None 102 | 103 | @property 104 | def interface(self): 105 | # get primary interface 106 | return SCDynamicStoreCopyValue(self.store, 'State:/Network/Global/IPv4')['PrimaryInterface'] 107 | 108 | @property 109 | def service(self): 110 | """ Returns the service relating to self.interface """ 111 | prefs = SCPreferencesCreate(kCFAllocatorDefault, 'PRG', None) 112 | 113 | # Fetch the list of services 114 | for serviceRef in SCNetworkServiceCopyAll(prefs): 115 | interface = SCNetworkServiceGetInterface(serviceRef) 116 | if self.interface == SCNetworkInterfaceGetBSDName(interface): 117 | return serviceRef 118 | return None 119 | 120 | def constructMenu(self): 121 | self.menu.removeAllItems() 122 | 123 | separator_required = False 124 | if self.is_ip_assigned: 125 | # For each of the proxyTypes we are concerned with, check to see if any 126 | # are configured. If so (even if not enabled), create a menuitem for 127 | # that proxy type. 128 | for proxy in self.proxyTypes.values(): 129 | enabled = CFDictionaryGetValue(self.prefDict, proxy['prefEnable']) 130 | if enabled is not None: 131 | proxy['menuitem'] = self.menu.addItemWithTitle_action_keyEquivalent_( 132 | proxy['title'], 133 | proxy['action'], 134 | proxy['keyEquivalent'] 135 | ) 136 | separator_required = True 137 | else: 138 | proxy['menuitem'] = None 139 | else: 140 | self.menu.addItemWithTitle_action_keyEquivalent_("No connection - Please connect to any network before using this tool", None, "") 141 | 142 | if separator_required: 143 | self.menu.addItem_(NSMenuItem.separatorItem()) 144 | 145 | # Need a way to quit 146 | self.menu.addItemWithTitle_action_keyEquivalent_("Quit", "quitApp:", "q") 147 | 148 | 149 | def watchForProxyOrIpChanges(self): 150 | """ install a watcher for proxy and Ip changes """ 151 | SCDynamicStoreSetNotificationKeys(self.store, None, ['State:/Network/Global/Proxies', 'State:/Network/Global/IPv4']) 152 | source = SCDynamicStoreCreateRunLoopSource(None, self.store, 0) 153 | loop = NSRunLoop.currentRunLoop().getCFRunLoop() 154 | CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) 155 | 156 | def dynamicStoreCallback(self, store, keys, info): 157 | """ callback for watcher """ 158 | self.log("Proxy or IP change detected") 159 | 160 | # could be an interface change, we have to rebuild menu from scratch in case the proxy configuration is different 161 | self.constructMenu() 162 | self.updateUI() 163 | self.setEnvVariables() 164 | 165 | def updateUI(self): 166 | if self.is_ip_assigned: 167 | self.log("Update proxy status on menu items") 168 | # load proxy dictionary 169 | proxydict = SCDynamicStoreCopyProxies(None) 170 | 171 | # get status for primary interface 172 | status = proxydict['__SCOPED__'][self.interface] 173 | 174 | # Are any proxies active now? 175 | anyProxyEnabled = False 176 | 177 | # update menu items according to their related proxy state 178 | for proxy in self.proxyTypes.values(): 179 | if proxy['menuitem']: 180 | proxy['menuitem'].setState_(status.get(proxy['prefEnable'], False) and NSOnState or NSOffState) 181 | if status.get(proxy['prefEnable'], False): 182 | anyProxyEnabled = True 183 | 184 | # set image 185 | self.statusitem.setImage_(anyProxyEnabled and self.active_image or self.inactive_image) 186 | else: 187 | self.statusitem.setImage_(self.no_network_image) 188 | 189 | def setEnvVariables(self): 190 | if self.is_ip_assigned: 191 | self.log("Setting env var according to system settings") 192 | # load proxy dictionary 193 | proxydict = SCDynamicStoreCopyProxies(None) 194 | # get status for primary interface 195 | status = proxydict['__SCOPED__'][self.interface] 196 | # update menu items according to their related proxy state 197 | for proxy in self.proxyTypes.values(): 198 | if proxy['menuitem'] and proxy['envVariable']: 199 | if status.get(proxy['prefEnable'], False): 200 | for envvar in proxy['envVariable']: 201 | self.executeCommand("launchctl setenv %s '%s'" % (envvar, "http://" + CFDictionaryGetValue(self.prefDict, proxy['prefProxy']) + ":" + str(CFDictionaryGetValue(self.prefDict, proxy['prefPort'])))) 202 | else: 203 | for envvar in proxy['envVariable']: 204 | self.executeCommand("launchctl unsetenv %s" % envvar) 205 | 206 | def quitApp_(self, sender): 207 | NSApp.terminate_(self) 208 | 209 | def toggleFtpProxy_(self, sender): 210 | self.toggleProxy(self.proxyTypes['ftp']['menuitem'], 'ftpproxy') 211 | 212 | def toggleHttpProxy_(self, sender): 213 | self.toggleProxy(self.proxyTypes['http']['menuitem'], 'webproxy') 214 | 215 | def toggleHttpsProxy_(self, sender): 216 | self.toggleProxy(self.proxyTypes['https']['menuitem'], 'securewebproxy') 217 | 218 | def toggleRtspProxy_(self, sender): 219 | self.toggleProxy(self.proxyTypes['rtsp']['menuitem'], 'streamingproxy') 220 | 221 | def toggleSocksProxy_(self, sender): 222 | self.toggleProxy(self.proxyTypes['socks']['menuitem'], 'socksfirewallproxy') 223 | 224 | def executeCommand(self, command): 225 | self.log("[Exec Command] %s" % command) 226 | commands.getoutput(command) 227 | 228 | def toggleProxy(self, item, target): 229 | """ callback for clicks on menu item """ 230 | servicename = SCNetworkServiceGetName(self.service) 231 | if not servicename: 232 | self.log("interface '%s' not found in services?" % self.interface) 233 | return 234 | 235 | newstate = item.state() == NSOffState and 'on' or 'off' 236 | 237 | # Sometimes the UI will be updated too fast if we don't wait a little 238 | # (resulting in wrongly enabled proxies in the menu), so after changing 239 | # the interface state we wait a bit (this is easier than doing a sleep 240 | # in code, as that has to be scheduled on the run loop) 241 | self.executeCommand("/usr/sbin/networksetup -set%sstate '%s' %s; sleep 1" % ( 242 | target, 243 | servicename, 244 | newstate 245 | )) 246 | self.updateUI() 247 | self.setEnvVariables() 248 | 249 | if __name__ == '__main__': 250 | sharedapp = NSApplication.sharedApplication() 251 | toggler = ToggleProxy.alloc().init() 252 | sharedapp.setDelegate_(toggler) 253 | sharedapp.run() 254 | -------------------------------------------------------------------------------- /resources/StatusBarImage-inactive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertklep/ToggleProxy/155fd0ba89069e03c95f8daf11c8b85a356dcb68/resources/StatusBarImage-inactive@2x.png -------------------------------------------------------------------------------- /resources/StatusBarImage-noNetwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertklep/ToggleProxy/155fd0ba89069e03c95f8daf11c8b85a356dcb68/resources/StatusBarImage-noNetwork@2x.png -------------------------------------------------------------------------------- /resources/StatusBarImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertklep/ToggleProxy/155fd0ba89069e03c95f8daf11c8b85a356dcb68/resources/StatusBarImage@2x.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from glob import glob 3 | import py2app, sys, os, commands 4 | 5 | # determine version by latest tag 6 | status, version = commands.getstatusoutput("git describe --abbrev=0 --tags") 7 | if status != 0: 8 | # probably no hg installed or not building from a repository 9 | version = "unknown" 10 | if version[0] == 'v': 11 | version = version[1:] 12 | 13 | setup( 14 | app = [ 'ToggleProxy.py' ], 15 | version = version, 16 | data_files = glob('resources/*.png'), 17 | options = dict(py2app = dict( 18 | plist = dict( 19 | CFBundleIdentifier = 'name.klep.toggleproxy', 20 | LSBackgroundOnly = True, 21 | ) 22 | )) 23 | ) 24 | --------------------------------------------------------------------------------