├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.markdown ├── TODO.markdown ├── bin └── oauth-proxy ├── oauth_proxy ├── __init__.py └── oauth_proxy.py ├── setup.py └── twisted └── plugins └── proxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.tap 2 | *.pyc 3 | dropin.cache 4 | twistd.* 5 | .Python 6 | bin/ 7 | *.egg-info 8 | include/ 9 | lib/ 10 | build/ 11 | dist/ 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010, Seth Fitzsimmons 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include twisted *.py 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # OAuth Proxy 2 | 3 | **NOTE**: If you're having trouble installing this, there's an [equivalent 4 | JavaScript version 5 | (mojodna/node-oauth-proxy)](https://github.com/mojodna/node-oauth-proxy) 6 | that's installed via `npm install -g oauth-proxy` (once you've installed 7 | [Node.js](http://nodejs.org/)). It's intended to be drop-in compatible. 8 | 9 | I am an OAuth proxy server. You can pass unsigned requests to me and I will 10 | sign them using [OAuth](http://oauth.net/ "OAuth") before sending them to 11 | their eventual destination. 12 | 13 | At the moment, tokens and consumer keys are configurable only at start-time, 14 | so individual proxies are limited to a single pair at a time. 2-legged OAuth 15 | (often used in lieu of API keys) is supported by omitting `--token` and 16 | `--token-secret` options. 17 | 18 | ## Installing 19 | 20 | Install via `easy_install`: 21 | 22 | $ easy_install oauth-proxy 23 | 24 | or `pip`: 25 | 26 | $ pip install oauth-proxy 27 | 28 | It will automatically download and install the Python OAuth lib (`oauth`) and 29 | Twisted (if necessary). 30 | 31 | ## Running 32 | 33 | Run the proxy with the provided `oauth-proxy` command: 34 | 35 | $ oauth-proxy \ 36 | --consumer-key \ 37 | --consumer-secret \ 38 | [--token ] \ 39 | [--token-secret ] \ 40 | [-p ] \ 41 | [--ssl] 42 | 43 | If you'd like to run the proxy as a daemon, run it with `twistd` directly: 44 | 45 | $ twistd oauth_proxy \ 46 | --consumer-key \ 47 | --consumer-secret \ 48 | [--token ] \ 49 | [--token-secret ] \ 50 | [-p ] \ 51 | [--ssl] 52 | 53 | ## Using 54 | 55 | This proxy can be used with command-line tools and web browsers alike. 56 | 57 | To use it with `curl`: 58 | 59 | $ curl -x localhost:8001 http://host.name/path 60 | 61 | To use it with `ab` (ApacheBench): 62 | 63 | $ ab -X localhost:8001 http://host.name/path 64 | 65 | To use it with Firefox, open the Network settings panel, under Advanced, and 66 | set a "Manual Proxy Configuration" after clicking the "Settings..." button. 67 | Ensure that "No Proxy for" does *not* include the host that you are attempting 68 | to explore. 69 | 70 | ## More Information 71 | 72 | More information on using this proxy, including instructions for obtaining 73 | access tokens, is available in [Exploring OAuth-Protected 74 | APIs](http://mojodna.net/2009/08/21/exploring-oauth-protected-apis.html). 75 | -------------------------------------------------------------------------------- /TODO.markdown: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Things that would be really nice to add to this tool. 4 | 5 | * Dynamic access tokens (according to / via Basic Auth, perhapss) 6 | * Restricted signing (i.e. only sign particular paths to avoid signing 7 | unnecessary paths) -------------------------------------------------------------------------------- /bin/oauth-proxy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | twistd -n oauth_proxy $* 4 | -------------------------------------------------------------------------------- /oauth_proxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mojodna/oauth-proxy/3d4a7403a68e61d0a3a24cee68e36c9e941ff83a/oauth_proxy/__init__.py -------------------------------------------------------------------------------- /oauth_proxy/oauth_proxy.py: -------------------------------------------------------------------------------- 1 | ## Implementation of an OAuth HTTP proxy 2 | # Adapted from Example 4-8, Twisted Network Programming Essentials by Abe Fettig. Copyright 2005 O'Reilly & Associates. 3 | # Adapted by Seth Fitzsimmons 4 | 5 | ## TODO 6 | # - provide a way of specifying access tokens (and possibly secrets, if not handled via lookup) - Basic Auth? 7 | 8 | import cgi 9 | from oauth import oauth 10 | import sgmllib, re, urlparse 11 | import sys 12 | 13 | # don't include SSL if it's not installed 14 | try: 15 | from twisted.internet import ssl 16 | except ImportError: 17 | pass 18 | 19 | from twisted.web import proxy, http 20 | from twisted.python import log, usage 21 | from zope.interface import implements, Interface 22 | 23 | class IOAuthCredentialProvider(Interface): 24 | """An OAuth credential provider""" 25 | 26 | def fetchCredentials(): 27 | """Fetch credentials""" 28 | 29 | 30 | class StaticOAuthCredentialProvider: 31 | implements(IOAuthCredentialProvider) 32 | 33 | def __init__(self, credentials): 34 | self.credentials = credentials 35 | 36 | def fetchCredentials(self): 37 | return self.credentials 38 | 39 | 40 | class OAuthCredentials: 41 | """ 42 | A container for OAuth credentials 43 | """ 44 | def __init__(self, consumerKey, consumerSecret, token = None, tokenSecret = None, signatureMethod = oauth.OAuthSignatureMethod_HMAC_SHA1()): 45 | self.oauthConsumer = oauth.OAuthConsumer(consumerKey, consumerSecret) 46 | 47 | if token is not None and tokenSecret is not None: 48 | self.oauthToken = oauth.OAuthToken(token, tokenSecret) 49 | else: 50 | self.oauthToken = None 51 | 52 | self.signatureMethod = signatureMethod 53 | 54 | 55 | class Options(usage.Options): 56 | synopsis = "--consumer-key --consumer-secret [--token ] [--token-secret ] [-p ] [--ssl]" 57 | longdesc = "An OAuth HTTP proxy server." 58 | optParameters = [ 59 | ['consumer-key', None, None, "OAuth Consumer Key"], 60 | ['consumer-secret', None, None, "OAuth Consumer Secret"], 61 | ['token', None, None, "OAuth Access/Request Token"], 62 | ['token-secret', None, None, "OAuth Access/Request Token Secret"], 63 | ['port', 'p', 8001, "Proxy port"], 64 | ] 65 | 66 | optFlags = [['ssl', 's']] 67 | 68 | def postOptions(self): 69 | if self['consumer-key'] is None or self['consumer-secret'] is None: 70 | raise usage.UsageError, "Your consumer key and secret must be provided." 71 | 72 | 73 | class OAuthProxyClient(proxy.ProxyClient): 74 | def connectionMade(self): 75 | # if retrieval of OAuth credentials is to be asynchronous, it needs to be done here (if it's even possible) 76 | # otherwise, this class has no point 77 | # however, it's possible that reading headers can't happen in OAuthProxyClientFactory 78 | proxy.ProxyClient.connectionMade(self) 79 | 80 | 81 | class OAuthProxyClientFactory(proxy.ProxyClientFactory): 82 | def buildProtocol(self, addr): 83 | credentials = self.father.credentialProvider.fetchCredentials() 84 | oauthRequest = self.signRequest(credentials) 85 | 86 | client = proxy.ProxyClientFactory.buildProtocol(self, addr) 87 | # upgrade proxy.proxyClient object to OAuthProxyClient 88 | client.__class__ = OAuthProxyClient 89 | client.factory = self 90 | 91 | client.headers.update(oauthRequest.to_header()) 92 | return client 93 | 94 | def signRequest(self, credentials): 95 | """Create an OAuthRequest and sign it""" 96 | 97 | if self.father.useSSL: 98 | path = self.father.path.replace("http", "https", 1) 99 | else: 100 | path = self.father.path 101 | 102 | # python parses arguments into a dict of arrays, e.g. 'q=foo' becomes {'q': ['foo']} 103 | # while from_consumer_and_token expects a dict of strings, so we cross our fingers, 104 | # hope there are no repeated arguments ('q=foo&q=bar'), and take the last value of 105 | # each array. 106 | args = dict((k,v[-1]) for k,v in self.father.args.items()) 107 | 108 | # create an OAuth Request from the pieces that we've assembled 109 | oauthRequest = oauth.OAuthRequest.from_consumer_and_token( 110 | oauth_consumer=credentials.oauthConsumer, 111 | token=credentials.oauthToken, 112 | http_method=self.father.method, 113 | http_url=path, 114 | parameters=args, 115 | ) 116 | 117 | # now, sign it 118 | oauthRequest.sign_request(credentials.signatureMethod, credentials.oauthConsumer, credentials.oauthToken) 119 | 120 | # TODO add X-Forwarded-For headers 121 | 122 | return oauthRequest 123 | 124 | 125 | class OAuthProxyRequest(proxy.ProxyRequest): 126 | protocols = {'http': OAuthProxyClientFactory} 127 | 128 | def __init__(self, credentialProvider, useSSL, *args): 129 | self.credentialProvider = credentialProvider 130 | self.useSSL = useSSL 131 | proxy.ProxyRequest.__init__(self, *args) 132 | 133 | if self.useSSL: 134 | # Since we magically mapped HTTP to HTTPS, we want to make sure that the transport knows as much 135 | self._forceSSL = True 136 | self.ports["http"] = 443 137 | 138 | # Copied from proxy.ProxyRequest just so the reactor connection can be SSL 139 | def process(self): 140 | headers = self.getAllHeaders().copy() 141 | if self.uri.startswith('/'): 142 | self.uri = 'http://' + headers['host'] + self.uri 143 | self.path = self.uri 144 | parsed = urlparse.urlparse(self.uri) 145 | protocol = parsed[0] 146 | host = parsed[1] 147 | port = self.ports[protocol] 148 | if ':' in host: 149 | host, port = host.split(':') 150 | port = int(port) 151 | rest = urlparse.urlunparse(('', '') + parsed[2:]) 152 | if not rest: 153 | rest = rest + '/' 154 | class_ = self.protocols[protocol] 155 | if 'host' not in headers: 156 | headers['host'] = host 157 | self.content.seek(0, 0) 158 | s = self.content.read() 159 | clientFactory = class_(self.method, rest, self.clientproto, headers, 160 | s, self) 161 | # The magic line for SSL support! 162 | if self.useSSL: 163 | self.reactor.connectSSL(host, port, clientFactory, ssl.ClientContextFactory()) 164 | else: 165 | self.reactor.connectTCP(host, port, clientFactory) 166 | 167 | 168 | class OAuthProxy(proxy.Proxy): 169 | def __init__(self, credentialProvider, useSSL): 170 | self.credentialProvider = credentialProvider 171 | self.useSSL = useSSL 172 | proxy.Proxy.__init__(self) 173 | 174 | def requestFactory(self, *args): 175 | return OAuthProxyRequest(self.credentialProvider, self.useSSL, *args) 176 | 177 | 178 | class OAuthProxyFactory(http.HTTPFactory): 179 | def __init__(self, credentialProvider, useSSL): 180 | self.credentialProvider = credentialProvider 181 | self.useSSL = useSSL 182 | http.HTTPFactory.__init__(self) 183 | 184 | def buildProtocol(self, addr): 185 | protocol = OAuthProxy(self.credentialProvider, self.useSSL) 186 | return protocol 187 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """OAuth Proxy""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="oauth-proxy", 7 | version="1.0.5", 8 | url="http://github.com/mojodna/oauth-proxy", 9 | license="BSD License", 10 | description="OAuth HTTP proxy", 11 | long_description="An OAuth proxy server that signs requests w/ provided tokens and passes them on to their original destination.", 12 | keywords="oauth proxy twisted", 13 | packages=['oauth_proxy', 'twisted.plugins'], 14 | package_data={ 15 | "twisted": ["plugins/proxy.py"] 16 | }, 17 | scripts=["bin/oauth-proxy"], 18 | install_requires=["twisted>=8.2.0", "oauth>=1.0.1"], 19 | author="Seth Fitzsimmons", 20 | author_email="seth@mojodna.net", 21 | classifiers=[ 22 | "Development Status :: 5 - Production/Stable", 23 | "Topic :: Utilities", 24 | "Topic :: Internet :: Proxy Servers", 25 | "License :: OSI Approved :: BSD License", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /twisted/plugins/proxy.py: -------------------------------------------------------------------------------- 1 | from zope.interface import implements 2 | 3 | from twisted.plugin import IPlugin 4 | from twisted.application.service import IServiceMaker 5 | from twisted.application import internet 6 | 7 | from oauth_proxy import oauth_proxy 8 | 9 | class OAuthProxyServiceMaker(object): 10 | implements(IServiceMaker, IPlugin) 11 | tapname = "oauth_proxy" 12 | description = "OAuth HTTP proxy" 13 | options = oauth_proxy.Options 14 | 15 | def makeService(self, options): 16 | # TODO add error handling for missing params 17 | 18 | useSSL = options["ssl"] 19 | 20 | consumerKey = options["consumer-key"] 21 | consumerSecret = options["consumer-secret"] 22 | if options.has_key("token") and options.has_key("token-secret"): 23 | token = options["token"] 24 | tokenSecret = options["token-secret"] 25 | else: 26 | token = tokenSecret = None 27 | 28 | port = int(options["port"]) 29 | 30 | credentials = oauth_proxy.OAuthCredentials(consumerKey, consumerSecret, token, tokenSecret) 31 | credentialProvider = oauth_proxy.StaticOAuthCredentialProvider(credentials) 32 | 33 | return internet.TCPServer(port, oauth_proxy.OAuthProxyFactory(credentialProvider, useSSL)) 34 | 35 | 36 | serviceMaker = OAuthProxyServiceMaker() 37 | --------------------------------------------------------------------------------