├── .gitignore ├── Procfile ├── README.md ├── app.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | cache -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app -w 3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GS Proxy 2 | 3 | A dead simple, caching proxy for Google spreadsheets. Uses Flask and meant to be deployed on Heroku. 4 | 5 | # Install and run locally 6 | 7 | 1. Create a virtualenv 8 | 2. ```pip install -r requirements.txt``` 9 | 3. Set spreadsheet keys that are acceptable: ```export GS_PROXY_KEYS=,,``` 10 | 4. Set how many minutes to keep cached values (default is 5): ```export GS_PROXY_CACHE=``` 11 | 5. Run locally: ```python app.py``` 12 | 6. Go to http://localhost:5000 13 | 14 | ## Deploy on Heroku 15 | 16 | For Heroku. 17 | 18 | 1. Setup and install Heroku command line tools 19 | 1. Create Heroku app with whatever name you want: ```heroku apps:create ``` 20 | 1. Add spreadsheet keys: ```heroku config:set GS_PROXY_KEYS=,,``` 21 | 1. (optional) Set cache time limit: ```heroku config:set GS_PROXY_CACHE=``` 22 | 1. Push up code: ```git push heroku master``` 23 | 1. You can open the app with ```heroku open``` 24 | 1. Use in your application by making a request like the following. Make sure to encode the proxy url parameter. ```http://.herokuapp.com/proxy?url=https%3A//spreadsheets.google.com/feeds/worksheets//public/basic%3Falt%3Djson-in-script``` -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import datetime 4 | import re 5 | import urlparse 6 | import urllib 7 | import requests 8 | from flask import Flask, Response, render_template, request, abort 9 | from flask.ext.cache import Cache 10 | 11 | # Get environement variables 12 | debug_app = 'DEBUG_APP' in os.environ 13 | proxy_keys = os.environ['GS_PROXY_KEYS'].split(',') 14 | proxy_cache = 5 15 | if 'GS_PROXY_CACHE' in os.environ: 16 | proxy_cache = int(os.environ['GS_PROXY_CACHE']) 17 | 18 | 19 | # Other application wide variables 20 | default_google_callback = 'gdata.io.handleScriptLoaded' 21 | domain_check = 'google' 22 | jsonp_to_json_keys = { 23 | 'json-in-script': 'json' 24 | } 25 | jsonp_header_overrides = { 26 | 'content-type': 'text/javascript; charset=UTF-8' 27 | } 28 | 29 | # Make and configure the Flask app 30 | app = Flask(__name__) 31 | if debug_app: 32 | app.debug = True 33 | 34 | 35 | # Set up cache 36 | cache_config = { 37 | 'CACHE_TYPE': 'filesystem', 38 | 'CACHE_THRESHOLD': 1000, 39 | 'CACHE_DIR': 'cache' 40 | } 41 | cache = Cache(config = cache_config) 42 | cache.init_app(app, config = cache_config) 43 | 44 | 45 | 46 | # Just a default route 47 | @app.route('/') 48 | @cache.cached(timeout = 500) 49 | def index(): 50 | return 'Supported keys: %s' % ', '.join(proxy_keys) 51 | 52 | 53 | # Proxy route 54 | @app.route('/proxy') 55 | def handle_proxy(): 56 | request_url = request.args.get('url', '') 57 | request_parsed = urlparse.urlparse(request_url) 58 | 59 | # Check if valid proxy url 60 | if not is_valid_url(request_parsed): 61 | abort(404) 62 | 63 | # Turn into JSON request and cache that 64 | request_url, callback = convert_jsonp_to_json(request_parsed) 65 | 66 | # Get value from proxied url (this is the cached part) 67 | proxy_request = make_proxy(request_url) 68 | if proxy_request['status_code'] != requests.codes.ok: 69 | abort(proxy_request['status_code']) 70 | 71 | # If callback, wrap response text and change some headers 72 | response_text = proxy_request['text'] 73 | if callback: 74 | response_text = '%s(%s);' % (callback, proxy_request['text']) 75 | proxy_request['headers'] = dict(proxy_request['headers'].items() + jsonp_header_overrides.items()) 76 | 77 | return Response(response_text, 78 | proxy_request['status_code'], proxy_request['headers']) 79 | 80 | 81 | # Get proxy URL and cache the results 82 | @cache.memoize(proxy_cache * 60) 83 | def make_proxy(url): 84 | if app.debug: 85 | print 'Cache missed: %s' % url 86 | 87 | r = requests.get(url) 88 | return { 89 | 'text': r.text, 90 | 'status_code': r.status_code, 91 | 'headers': r.headers, 92 | } 93 | 94 | 95 | # Convert call to json request and get callback 96 | def convert_jsonp_to_json(url_parsed): 97 | # Parses query string into list of tuples 98 | query_parsed = urlparse.parse_qsl(url_parsed.query) 99 | 100 | # Get callback if there is one 101 | callback = False 102 | for i, v in enumerate(query_parsed): 103 | if query_parsed[i][0] == 'callback': 104 | callback = query_parsed[i][1] 105 | del query_parsed[i] 106 | 107 | # Convert jsonp request to json 108 | jsonp_found = False 109 | 110 | for k in jsonp_to_json_keys: 111 | for i, v in enumerate(query_parsed): 112 | if query_parsed[i][0] == 'alt' and query_parsed[i][1] == k: 113 | query_parsed[i] = ('alt', jsonp_to_json_keys[k]) 114 | jsonp_found = True 115 | 116 | # If no callback and jsonp, set default 117 | if jsonp_found and not callback: 118 | callback = default_google_callback 119 | 120 | # Recreate the query string by making new tuple. 121 | # Note that we are reseting the fragment 122 | # and any the other probably unnecessary parts. 123 | new_url_tuple = url_parsed[0:4] + (urllib.urlencode(query_parsed),) + ('',) 124 | 125 | return urlparse.urlunparse(new_url_tuple), callback 126 | 127 | 128 | # Check if valid key is in url 129 | def is_valid_url(url_parsed): 130 | # Make sure the key is in the path, and not the 131 | # query string, otherwise the service could be abused. 132 | # As an extra mesaure, just make sure the domain 133 | # is google. 134 | found = False 135 | 136 | for k in proxy_keys: 137 | if url_parsed.path.find(k) != -1: 138 | found = True 139 | 140 | if url_parsed.netloc.find(domain_check) == -1: 141 | found = False 142 | 143 | return found; 144 | 145 | 146 | # Start Flask App 147 | if __name__ == '__main__': 148 | # Bind to PORT if defined, otherwise default to 5000. 149 | port = int(os.environ.get('PORT', 5000)) 150 | app.run(host='0.0.0.0', port=port) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.9 2 | Flask-Cache==0.10.1 3 | gunicorn==0.17.2 4 | requests==1.1.0 5 | --------------------------------------------------------------------------------