├── handlers ├── tasks │ ├── __init__.py │ ├── feedping.py │ └── update.py ├── __init__.py ├── pac_config.py ├── gfwtest.py ├── changelog.py └── pac_generate.py ├── static ├── pass.png ├── block.png ├── favicon.ico ├── robots.txt ├── feed_email.gif ├── share_badge.gif ├── script.js ├── tipTip.css ├── styles.css └── jquery.tipTip.minified.js ├── .gitignore ├── .gitmodules ├── cron.yaml ├── util ├── __init__.py ├── useragent.py ├── template.py ├── memcache.py └── webcache.py ├── templates ├── usage.html ├── 2column.html ├── changelogRssItem.html ├── gfwtest.html ├── base.html └── index.html ├── models ├── usersetting.py └── __init__.py ├── index.yaml ├── app.yaml ├── tools └── dev_server_load_data ├── settings.py ├── main.py └── autoproxy2pac.py /handlers/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | import feedping 2 | import update 3 | -------------------------------------------------------------------------------- /static/pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamac/autoproxy2pac/HEAD/static/pass.png -------------------------------------------------------------------------------- /static/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamac/autoproxy2pac/HEAD/static/block.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamac/autoproxy2pac/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /pac/ 3 | Disallow: /gfwtest.js 4 | Allow: / 5 | -------------------------------------------------------------------------------- /static/feed_email.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamac/autoproxy2pac/HEAD/static/feed_email.gif -------------------------------------------------------------------------------- /static/share_badge.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamac/autoproxy2pac/HEAD/static/share_badge.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse pydev files 2 | .project 3 | .pydevproject 4 | .settings/ 5 | 6 | settings2.py 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "static/facebox"] 2 | path = static/facebox 3 | url = git://github.com/defunkt/facebox.git 4 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: Update rules regularly 3 | url: /tasks/update 4 | schedule: every 30 minutes 5 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | import tasks 2 | 3 | import changelog 4 | import gfwtest 5 | import pac_config 6 | import pac_generate 7 | -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- 1 | import template 2 | import useragent 3 | from memcache import memcached, responsecached 4 | from webcache import webcached 5 | -------------------------------------------------------------------------------- /util/useragent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | def family(): 6 | ua = os.getenv('HTTP_USER_AGENT') 7 | 8 | if 'MSIE' in ua: 9 | return 'IE' 10 | elif 'Chrome' in ua: 11 | return 'Chrome' 12 | else: 13 | return None 14 | -------------------------------------------------------------------------------- /templates/usage.html: -------------------------------------------------------------------------------- 1 | {% if url %} 2 | 使用方法:将浏览器的“代理服务器自动配置脚本”设置为 {{url}} 3 | {% else %} 4 | {% ifequal browser "IE" %} 5 | 使用方法:Internet选项 - 连接 - 局域网设置,勾选“使用自动配置脚本”,“地址”设置为 file://c:/autoproxy.pac (请根据下载位置进行调整) 6 | {% else %} 7 | 使用方法:将浏览器的“代理服务器自动配置脚本”指向下载到的文件 8 | {% endifequal %} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /models/usersetting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.ext import db 4 | 5 | class UserSetting(db.Model): 6 | defaultProxy = db.StringProperty(required=True) 7 | pacName = db.StringProperty(required=True) 8 | customRules = db.StringListProperty() 9 | lastModified = db.DateTimeProperty(required=True, auto_now=True) 10 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | /* Two-column template */ 2 | function loadChangelog(){ 3 | $('div.feedburnerFeedBlock h3').each(function(){ 4 | $(this).next('ul').children('li').each($(this).text() != '规则调整' ? function(){ 5 | var item = $(this).html(); 6 | $(this).html(item.slice(0, item.indexOf('('))); 7 | } 8 | : function(){ 9 | var item = $(this).html(); 10 | $(this).html(item.slice(item.indexOf('(') + 1, item.indexOf(')'))); 11 | }); 12 | $(this).remove(); 13 | }); 14 | $('div.feedburnerFeedBlock').replaceAll('#changelog p').fadeIn(); 15 | } 16 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | - kind: ChangeLog 14 | properties: 15 | - name: ruleList 16 | - name: date 17 | direction: desc 18 | -------------------------------------------------------------------------------- /handlers/tasks/feedping.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import xmlrpclib 5 | from google.appengine.ext import webapp 6 | 7 | class FeedBurnerHandler(webapp.RequestHandler): 8 | ''' 9 | Ping FeedBurner to update the feed immediately 10 | @see: http://feedburner.google.com/fb/a/ping 11 | ''' 12 | def post(self): 13 | url = self.request.get('url') 14 | 15 | try: 16 | rpc = xmlrpclib.ServerProxy('http://ping.feedburner.google.com/') 17 | result = rpc.weblogUpdates.ping('', url) 18 | 19 | if result['flerror']: raise xmlrpclib.Fault(1, result['message']) 20 | except xmlrpclib.Error, e: 21 | logging.error('Ping FeedBurner for %s failed: %s', url, e) 22 | self.error(500) 23 | return 24 | 25 | logging.debug('Pinged FeedBurner for %s', url) 26 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: autoproxy2pac 2 | version: 4 3 | runtime: python 4 | api_version: 1 5 | 6 | default_expiration: 1d 7 | 8 | handlers: 9 | 10 | - url: /(robots.txt|favicon.ico) 11 | static_files: static/\1 12 | upload: static/(robots.txt|favicon.ico) 13 | 14 | - url: /static 15 | static_dir: static 16 | 17 | - url: /facebox 18 | static_dir: static/facebox 19 | 20 | - url: /tasks/.* 21 | script: main.py 22 | login: admin 23 | 24 | - url: /_ah/admin(/.*)? 25 | script: $PYTHON_LIB/google/appengine/ext/admin 26 | login: admin 27 | 28 | - url: /remote_api 29 | script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py 30 | login: admin 31 | 32 | - url: .* 33 | script: main.py 34 | 35 | admin_console: 36 | pages: 37 | 38 | - name: Interactive Console 39 | url: /_ah/admin/interactive 40 | 41 | - name: Memcache Viewer 42 | url: /_ah/admin/memcache 43 | -------------------------------------------------------------------------------- /handlers/tasks/update.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | from google.appengine.ext import webapp 5 | from google.appengine.api import memcache 6 | from google.appengine.api.labs import taskqueue 7 | 8 | from models import RuleList 9 | from settings import MAIN_SERVER 10 | 11 | class Handler(webapp.RequestHandler): 12 | def get(self): 13 | for name, url in (('gfwlist', 'http://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt'),): 14 | r = RuleList.getList(name) 15 | if r == None: 16 | r = RuleList(name=name, url=url) 17 | 18 | if r.update(): 19 | logging.info('%s updated to %s' , name, r.date) 20 | 21 | if MAIN_SERVER: 22 | if name == 'gfwlist': memcache.delete('/gfwtest.js', namespace='response') 23 | memcache.delete('changelog/%s' % name) 24 | taskqueue.add(url='/tasks/feed_ping', params={'url':'http://feeds.feedburner.com/%s' % name}) 25 | -------------------------------------------------------------------------------- /tools/dev_server_load_data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while getopts "p:" opt; do 4 | case $opt in 5 | p) proxy=$OPTARG; shift;; 6 | esac 7 | shift 8 | done 9 | 10 | DEV_SERVER_PORT=${1:-8080} 11 | DEV_SERVER=http://localhost:${DEV_SERVER_PORT} 12 | 13 | if ! curl -s -I $DEV_SERVER > /dev/null; then echo "Development server is not running"; exit; fi 14 | 15 | read -p "Account: " user 16 | read -s -p "Password: " pass 17 | 18 | for kind in RuleList ChangeLog 19 | do 20 | dumpfile=`mktemp -t dump_$kind` 21 | rm $dumpfile 22 | 23 | HTTP_PROXY=$1 24 | echo $pass | bulkloader.py --dump --kind=$kind --url=http://autoproxy2pac.appspot.com/remote_api --email=$user --passin --filename=$dumpfile --log_file=$dumpfile.log --db_filename=$dumpfile.db --result_db_filename=$dumpfile.results.db 25 | if [ $? -ne 0 ]; then exit; fi 26 | 27 | HTTP_PROXY= 28 | echo | bulkloader.py --restore --kind=$kind --url=${DEV_SERVER}/remote_api --app_id=autoproxy2pac --email=test --passin --filename=$dumpfile --log_file=/dev/null --db_filename=skip 29 | done 30 | -------------------------------------------------------------------------------- /util/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from datetime import datetime 5 | from google.appengine.api import users 6 | from google.appengine.ext.webapp import template 7 | 8 | import settings 9 | 10 | def render(template_, **param): 11 | currentUrl = os.getenv('PATH_INFO') 12 | template_path = os.path.join(settings.TEMPLATE_DIR, template_) 13 | template_dict = { 14 | 'gfwlist_rss': '/changelog/gfwlist.rss', 15 | 'is_dev': settings.DEBUG, 16 | 'language': 'zh-CN', 17 | 'login_url': users.create_login_url(currentUrl), 18 | 'logout_url': users.create_logout_url(currentUrl), 19 | 'media_url': settings.MEDIA_URL, 20 | 'user': users.get_current_user(), 21 | 'url_protocol': 'https://' if os.getenv('HTTPS') == 'on' else 'http://', 22 | } 23 | template_dict.update(param) 24 | return template.render(template_path, template_dict, debug=settings.TEMPLATE_DEBUG) 25 | 26 | def mtime(template_): 27 | return datetime.fromtimestamp(os.stat(os.path.join(settings.TEMPLATE_DIR, template_)).st_mtime) 28 | -------------------------------------------------------------------------------- /templates/2column.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | 3 | {%block content%} 4 |
5 | {%block main%}BODY GOES HERE{%endblock%} 6 |
7 | 8 | 23 | 24 |
25 | {%endblock%} 26 | 27 | {%block deferred_script%} 28 | {%block deferred_script2%}{%endblock%} 29 | 30 | 31 | 32 | {%endblock%} 33 | -------------------------------------------------------------------------------- /templates/changelogRssItem.html: -------------------------------------------------------------------------------- 1 | {% for item in unblock %} 2 | {% if forloop.first %}

解封网址

{% endif %} 6 | {% endfor %} 7 | 8 | {% for item in block %} 9 | {% if forloop.first %}

封锁网址

{% endif %} 13 | {% endfor %} 14 | 15 | {% for item in rule_adjust %} 16 | {% if forloop.first %}

规则调整

{% endif %} 21 | {% endfor %} 22 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | DEBUG = os.getenv('SERVER_SOFTWARE', 'Dev').startswith('Dev') 6 | 7 | MAIN_SERVER = os.getenv('APPLICATION_ID', 'autoproxy2pac') == 'autoproxy2pac' 8 | 9 | TEMPLATE_DEBUG = DEBUG 10 | 11 | TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates') 12 | 13 | MEDIA_URL = '/static/' 14 | 15 | CACHE_ENABLED = not DEBUG 16 | 17 | RATELIMIT_ENABLED = True 18 | 19 | PAC_URL_PREFIX = 'pac/' if MAIN_SERVER else '' 20 | 21 | PAC_USER_URL_PREFIX = 'u/' 22 | 23 | PRESET_PROXIES = { 24 | 'gappproxy' : ('GAppProxy', 'PROXY 127.0.0.1:8000'), 25 | 'tor' : ('Tor', 'PROXY 127.0.0.1:8118; SOCKS 127.0.0.1:9050'), 26 | 'jap' : ('JAP', 'PROXY 127.0.0.1:4001'), 27 | 'your-freedom' : ('Your Freedom', 'PROXY 127.0.0.1:8080'), 28 | 'wu-jie' : ('无界', 'PROXY 127.0.0.1:9666'), 29 | 'free-gate' : ('自由门', 'PROXY 127.0.0.1:8580'), 30 | 'puff' : ('Puff', 'PROXY 127.0.0.1:1984'), 31 | 'privoxy' : ('Privoxy + SOCKS', 'PROXY 127.0.0.1:8118'), 32 | 'ssh-d' : ('ssh -D / MyEnTunnel', 'SOCKS 127.0.0.1:7070'), 33 | } 34 | 35 | MAX_CUSTOM_RULE_NUMBER_FOR_MIRROR = 10 36 | 37 | try: 38 | # Settings not under version control 39 | from settings2 import * 40 | except ImportError: 41 | # Base URL of the mirrors, None stands for the main server itself 42 | MIRRORS = (None,) 43 | 44 | # QUOTA times of retrieval per DURATION (unit: hour) is allowed in maximum 45 | RATELIMIT_DURATION = 0 46 | RATELIMIT_QUOTA = lambda ip, ua: 0 47 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import logging 5 | from google.appengine.ext import webapp 6 | from google.appengine.ext.webapp.util import run_wsgi_app 7 | 8 | from settings import DEBUG, MAIN_SERVER, CACHE_ENABLED, RATELIMIT_ENABLED, PAC_URL_PREFIX 9 | from handlers import * 10 | 11 | # Log a message each time this module get loaded. 12 | logging.debug( 13 | 'Loading %s %s, MAIN_SERVER = %s, CACHE_ENABLED = %s, RATELIMIT_ENABLED = %s, PAC_URL_PREFIX = "%s"', 14 | os.getenv('APPLICATION_ID'), os.getenv('CURRENT_VERSION_ID'), 15 | MAIN_SERVER, CACHE_ENABLED, RATELIMIT_ENABLED, PAC_URL_PREFIX, 16 | ) 17 | 18 | # A hack to be able to get the status of a Response instance, read-only 19 | webapp.Response.status = property(lambda self: self._Response__status[0]) 20 | 21 | urlMapping = [ 22 | ('/tasks/update', tasks.update.Handler), 23 | ('/%s(.+)' % PAC_URL_PREFIX, pac_generate.Handler), 24 | ] 25 | if MAIN_SERVER: urlMapping += [ 26 | ('/', pac_config.MainHandler), 27 | ('/usage', pac_config.UsageHandler), 28 | ('/gfwtest.js', gfwtest.JsLibHandler), 29 | ('/gfwtest', gfwtest.TestPageHandler), 30 | ('/changelog/(.*)\.rss', changelog.FeedHandler), 31 | ('/tasks/feed_ping', tasks.feedping.FeedBurnerHandler), 32 | ] 33 | application = webapp.WSGIApplication(urlMapping, DEBUG) 34 | 35 | def main(): 36 | if DEBUG: logging.getLogger().setLevel(logging.DEBUG) 37 | 38 | if os.getenv('AUTH_DOMAIN') != 'gmail.com': 39 | logging.warn('Fixing auth domain (%r)', os.getenv('AUTH_DOMAIN')) 40 | os.environ['AUTH_DOMAIN'] = 'gmail.com' 41 | 42 | run_wsgi_app(application) 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /handlers/pac_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.api import users 4 | from google.appengine.ext import webapp 5 | 6 | from models import UserSetting 7 | from settings import PAC_URL_PREFIX, PAC_USER_URL_PREFIX, PRESET_PROXIES 8 | from util import template, useragent, webcached 9 | 10 | class MainHandler(webapp.RequestHandler): 11 | @webcached(('public,max-age=3600', 'private,max-age=3600'), 'Cookie') # 1h 12 | def get(self): 13 | user = users.get_current_user() 14 | 15 | self.lastModified(template.mtime('index.html')) 16 | self.response.out.write(template.render('index.html', 17 | presetProxies=((k, v[0]) for k, v in PRESET_PROXIES.items()), 18 | pacUrlPrefix=PAC_URL_PREFIX, 19 | pacUserUrlPrefix=PAC_USER_URL_PREFIX, 20 | userSetting=UserSetting.get_by_key_name(user.user_id()) if user else None, 21 | )) 22 | 23 | def post(self): 24 | user = users.get_current_user() 25 | if not user or not self.request.get('customize'): return 26 | 27 | pacName = self.request.get('pacname', '').lower() 28 | if pacName != user.nickname().lower(): 29 | self.error(400) 30 | return 31 | 32 | UserSetting( 33 | key_name=user.user_id(), 34 | defaultProxy=self.request.get('proxy'), 35 | pacName=pacName, 36 | customRules=self.request.get('addrules').splitlines(), 37 | ).put() 38 | 39 | if self.request.get('usage') != 'online': 40 | self.redirect('/%s%s%s?download' % (PAC_URL_PREFIX, PAC_USER_URL_PREFIX, pacName), permanent=False) 41 | 42 | class UsageHandler(webapp.RequestHandler): 43 | @webcached('public,max-age=86400') # 24h 44 | def get(self): 45 | self.lastModified(template.mtime('usage.html')) 46 | 47 | url = self.request.get('u') 48 | if url: url = 'http://%s/%s%s' % (self.request.host, PAC_URL_PREFIX, url) 49 | 50 | self.response.out.write(template.render('usage.html', 51 | url=url, 52 | browser=useragent.family(), 53 | )) 54 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.ext import db 4 | from google.appengine.api import memcache 5 | 6 | import autoproxy2pac 7 | from util import memcached 8 | 9 | class RuleList(db.Model): 10 | name = db.StringProperty(required=True) 11 | url = db.LinkProperty(required=True) 12 | date = db.StringProperty() 13 | raw = db.TextProperty() 14 | code = db.TextProperty() 15 | 16 | def update(self): 17 | rawOld = self.raw 18 | self.raw, timestamp = autoproxy2pac.fetchRuleList(self.url) 19 | if timestamp == self.date: return False 20 | 21 | self.code = autoproxy2pac.rule2js(self.raw) 22 | self.date = timestamp 23 | memcache.set(self.name, self, namespace='rule') 24 | self.put() 25 | 26 | if rawOld: 27 | diff = ChangeLog.new(self, rawOld, self.raw) 28 | if diff: diff.put() 29 | 30 | return True 31 | 32 | def toDict(self): 33 | return { 'ruleListUrl' : self.url, 34 | 'ruleListDate' : self.date, 35 | 'ruleListCode' : self.code } 36 | 37 | @classmethod 38 | @memcached(key=(lambda _, name:name), namespace='rule') 39 | def getList(cls, name): 40 | return cls.gql('WHERE name=:1', name).get() 41 | 42 | class ChangeLog(db.Model): 43 | ruleList = db.ReferenceProperty(RuleList, required=True) 44 | date = db.DateTimeProperty(auto_now_add=True) 45 | add = db.StringListProperty() 46 | remove = db.StringListProperty() 47 | 48 | @classmethod 49 | def new(cls, ruleList, old, new): 50 | ret = ChangeLog(ruleList=ruleList) 51 | 52 | from difflib import SequenceMatcher 53 | toSeq = lambda raw: [l for l in raw.splitlines()[1:] if l and not l.startswith('!')] 54 | old = toSeq(old) 55 | new = toSeq(new) 56 | for tag, i1, i2, j1, j2 in SequenceMatcher(a=old, b=new).get_opcodes(): 57 | if tag != 'equal': 58 | ret.remove.extend(old[i1:i2]) 59 | ret.add.extend(new[j1:j2]) 60 | 61 | # Ignore unmodified rules (just moved to another place) 62 | for line in set(ret.add).intersection(set(ret.remove)): 63 | ret.add.remove(line) 64 | ret.remove.remove(line) 65 | 66 | if ret.add or ret.remove: 67 | return ret 68 | else: 69 | return None 70 | 71 | from usersetting import UserSetting 72 | -------------------------------------------------------------------------------- /handlers/gfwtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.ext import webapp 4 | 5 | import autoproxy2pac 6 | from models import RuleList 7 | from util import template, webcached, responsecached 8 | 9 | jsFileTemplate = '''/* 10 | * Provide a javascript function to determine whether a URL is blocked in mainland China 11 | * You can get this file at http://autoproxy2pac.appspot.com/gfwtest.js 12 | * 13 | * Usage: isBlockedByGFW(url), returns true if the URL is blocked 14 | * 15 | * Last update: %(ruleListDate)s 16 | */ 17 | 18 | // Base64 code from Tyler Akins -- http://rumkin.com 19 | function decode64(_1){var _2="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var _3="";var _4,_5,_6;var _7,_8,_9,_a;var i=0;_1=_1.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{_7=_2.indexOf(_1.charAt(i++));_8=_2.indexOf(_1.charAt(i++));_9=_2.indexOf(_1.charAt(i++));_a=_2.indexOf(_1.charAt(i++));_4=(_7<<2)|(_8>>4);_5=((_8&15)<<4)|(_9>>2);_6=((_9&3)<<6)|_a;_3=_3+String.fromCharCode(_4);if(_9!=64){_3=_3+String.fromCharCode(_5);}if(_a!=64){_3=_3+String.fromCharCode(_6);}}while(i<_1.length);return _3;}; 20 | 21 | // Encode the function using Base64 for security purpose 22 | eval(decode64("%(encodedFunc)s")) 23 | ''' 24 | 25 | jsFuncTemplate = '''function isBlockedByGFW(url) { 26 | url = encodeURI(url).replace(/%%25/g,'%%'); 27 | var %(proxyVar)s = true; 28 | var %(defaultVar)s = false; 29 | 30 | %(ruleListCode)s 31 | 32 | return %(defaultVar)s; 33 | } 34 | ''' 35 | 36 | def generateJs(rules): 37 | import base64 38 | data = { 'proxyVar' : autoproxy2pac.proxyVar, 39 | 'defaultVar' : autoproxy2pac.defaultVar } 40 | data.update(rules) 41 | data['encodedFunc'] = base64.b64encode(jsFuncTemplate % data) 42 | return jsFileTemplate % data 43 | 44 | class JsLibHandler(webapp.RequestHandler): 45 | @webcached('public,max-age=600') # 10min 46 | @responsecached() 47 | def get(self): 48 | rules = RuleList.getList('gfwlist') 49 | if rules is None: 50 | self.error(500) 51 | return 52 | 53 | self.lastModified(rules.date) 54 | self.response.headers['Content-Type'] = 'application/x-javascript' 55 | self.response.out.write(generateJs(rules.toDict())) 56 | 57 | class TestPageHandler(webapp.RequestHandler): 58 | @webcached('public,max-age=86400', 'Cookie') # 24h 59 | def get(self): 60 | self.lastModified(template.mtime('gfwtest.html')) 61 | self.response.out.write(template.render('gfwtest.html')) 62 | -------------------------------------------------------------------------------- /static/tipTip.css: -------------------------------------------------------------------------------- 1 | /* TipTip CSS - Version 1.2 */ 2 | 3 | #tiptip_holder { 4 | display: none; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | z-index: 99999; 9 | } 10 | 11 | #tiptip_holder.tip_top { 12 | padding-bottom: 5px; 13 | } 14 | 15 | #tiptip_holder.tip_bottom { 16 | padding-top: 5px; 17 | } 18 | 19 | #tiptip_holder.tip_right { 20 | padding-left: 5px; 21 | } 22 | 23 | #tiptip_holder.tip_left { 24 | padding-right: 5px; 25 | } 26 | 27 | #tiptip_content { 28 | font-size: 11px; 29 | color: #fff; 30 | text-shadow: 0 0 2px #000; 31 | padding: 4px 8px; 32 | border: 1px solid rgba(255,255,255,0.25); 33 | background-color: rgb(25,25,25); 34 | background-color: rgba(25,25,25,0.92); 35 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(transparent), to(#000)); 36 | border-radius: 3px; 37 | -webkit-border-radius: 3px; 38 | -moz-border-radius: 3px; 39 | box-shadow: 0 0 3px #555; 40 | -webkit-box-shadow: 0 0 3px #555; 41 | -moz-box-shadow: 0 0 3px #555; 42 | } 43 | 44 | #tiptip_arrow, #tiptip_arrow_inner { 45 | position: absolute; 46 | border-color: transparent; 47 | border-style: solid; 48 | border-width: 6px; 49 | height: 0; 50 | width: 0; 51 | } 52 | 53 | #tiptip_holder.tip_top #tiptip_arrow { 54 | border-top-color: #fff; 55 | border-top-color: rgba(255,255,255,0.35); 56 | } 57 | 58 | #tiptip_holder.tip_bottom #tiptip_arrow { 59 | border-bottom-color: #fff; 60 | border-bottom-color: rgba(255,255,255,0.35); 61 | } 62 | 63 | #tiptip_holder.tip_right #tiptip_arrow { 64 | border-right-color: #fff; 65 | border-right-color: rgba(255,255,255,0.35); 66 | } 67 | 68 | #tiptip_holder.tip_left #tiptip_arrow { 69 | border-left-color: #fff; 70 | border-left-color: rgba(255,255,255,0.35); 71 | } 72 | 73 | #tiptip_holder.tip_top #tiptip_arrow_inner { 74 | margin-top: -7px; 75 | margin-left: -6px; 76 | border-top-color: rgb(25,25,25); 77 | border-top-color: rgba(25,25,25,0.92); 78 | } 79 | 80 | #tiptip_holder.tip_bottom #tiptip_arrow_inner { 81 | margin-top: -5px; 82 | margin-left: -6px; 83 | border-bottom-color: rgb(25,25,25); 84 | border-bottom-color: rgba(25,25,25,0.92); 85 | } 86 | 87 | #tiptip_holder.tip_right #tiptip_arrow_inner { 88 | margin-top: -6px; 89 | margin-left: -5px; 90 | border-right-color: rgb(25,25,25); 91 | border-right-color: rgba(25,25,25,0.92); 92 | } 93 | 94 | #tiptip_holder.tip_left #tiptip_arrow_inner { 95 | margin-top: -6px; 96 | margin-left: -7px; 97 | border-left-color: rgb(25,25,25); 98 | border-left-color: rgba(25,25,25,0.92); 99 | } 100 | 101 | /* Webkit Hacks */ 102 | @media screen and (-webkit-min-device-pixel-ratio:0) { 103 | #tiptip_content { 104 | padding: 4px 8px 5px 8px; 105 | background-color: rgba(45,45,45,0.88); 106 | } 107 | #tiptip_holder.tip_bottom #tiptip_arrow_inner { 108 | border-bottom-color: rgba(45,45,45,0.88); 109 | } 110 | #tiptip_holder.tip_top #tiptip_arrow_inner { 111 | border-top-color: rgba(20,20,20,0.92); 112 | } 113 | } -------------------------------------------------------------------------------- /util/memcache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | from functools import wraps 5 | from google.appengine.api import memcache 6 | from google.appengine.api import users 7 | 8 | from settings import CACHE_ENABLED 9 | 10 | class memcached(object): 11 | ''' 12 | Decorate any function or method whose return value to keep in memcache 13 | 14 | @param key: Can be a string or a function (takes the same arguments as the 15 | wrapped function, and returns a string key) 16 | @param time: Optional expiration time, either relative number of seconds from 17 | current time (up to 1 month), or an absolute Unix epoch time 18 | @param namespace: An optional namespace for the key 19 | 20 | @note: Set CACHE_ENABLED to False to globally disable memcache 21 | @note: Won't cache if the inner function returns None 22 | ''' 23 | def __init__(self, key, time=0, namespace=None): 24 | self.key = key 25 | self.time = time 26 | self.namespace = namespace 27 | 28 | def __call__(self, f): 29 | @wraps(f) 30 | def wrapped(*args, **kwargs): 31 | key = self.key(*args, **kwargs) if callable(self.key) else self.key 32 | 33 | data = memcache.get(key, namespace=self.namespace) 34 | if data is not None: return data 35 | 36 | logging.debug('Memcache for %s missed, key = %s@%s', f.__name__, key, self.namespace) 37 | data = f(*args, **kwargs) 38 | if data is not None: memcache.set(key, data, self.time, namespace=self.namespace) 39 | return data 40 | 41 | return wrapped if CACHE_ENABLED else f 42 | 43 | class responsecached(object): 44 | ''' 45 | Decorate RequestHandler.get/post/etc. to keep the response in memcache 46 | A convenient wrapper of memcached 47 | 48 | @note: Multiple memcache items may be generated using the default key algorithm 49 | ''' 50 | def __init__(self, time=0, key=None, namespace='response', cacheableStatus=(200,), onlyAnonymous=False): 51 | self.time = time 52 | self.key = key if key else lambda h, *_: h.request.path_qs 53 | self.namespace = namespace 54 | self.cacheableStatus = cacheableStatus 55 | self.onlyAnonymous = onlyAnonymous 56 | 57 | def __call__(self, f): 58 | @wraps(f) 59 | def wrapped(handler, *args): 60 | if self.onlyAnonymous and users.get_current_user(): 61 | f(handler, *args) 62 | return 63 | 64 | @memcached(self.key, self.time, self.namespace) 65 | def getResponse(handler, *args): 66 | f(handler, *args) 67 | return handler.response if handler.response.status in self.cacheableStatus else None 68 | 69 | # In `WSGIApplication.__call__`, `handler.response` is just a reference 70 | # of the local variable `response`, whose `wsgi_write` method is called. 71 | # So just assign a new response object to `handler.response` will not work. 72 | handler.response.__dict__ = getResponse(handler, *args).__dict__ 73 | 74 | return wrapped 75 | -------------------------------------------------------------------------------- /templates/gfwtest.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html"%} 2 | 3 | {%block title1%}GFW Test{%endblock%} 4 | 5 | {%block css_and_script%} 6 | 7 | 26 | {%endblock%} 27 | 28 | {%block deferred_script%} 29 | 30 | 65 | 66 | {%endblock%} 67 | 68 | {%block tagline%} 69 | 查看网站是否撞墙 70 | {%endblock%} 71 | 72 | {%block content%} 73 |
74 | 75 |
76 |
77 |
78 | 82 |
83 |

试试这些: 84 | Google 85 | Facebook 86 | YouTube 87 | Twitter 88 | Wikipedia 89 | Blogger 90 | Picasa 91 |

92 | {%endblock%} 93 | -------------------------------------------------------------------------------- /util/webcache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from calendar import timegm 4 | from datetime import datetime 5 | from email.utils import formatdate 6 | from functools import wraps 7 | from hashlib import md5 8 | from types import MethodType 9 | from google.appengine.api import users 10 | 11 | class _ResponseNotModified(Exception): 12 | pass 13 | 14 | def _lastModified(handler, time): 15 | if isinstance(time, datetime): 16 | time = formatdate(timegm(time.timetuple()), False, True) 17 | handler.response.headers['Last-Modified'] = time 18 | 19 | if _validate(handler): raise _ResponseNotModified 20 | 21 | def _validate(handler): 22 | ''' 23 | Validate client cache. Last-Modified and/or ETag headers should be set. 24 | 25 | @return: True if the page is cached (no need to generate the content) 26 | ''' 27 | if handler.response.status != 200: return False 28 | 29 | # @see: http://tools.ietf.org/html/rfc2616#section-14.25 30 | ims = handler.request.headers.get('If-Modified-Since') 31 | if ims: 32 | lm = handler.response.headers.get('Last-Modified') 33 | if lm is None or ims != lm: return False 34 | 35 | # @see: http://tools.ietf.org/html/rfc2616#section-14.26 36 | inm = (t.strip('" ') for t in handler.request.headers.get('If-None-Match', '').split(',')) 37 | if inm: 38 | et = handler.response.headers.get('ETag', '').strip('"') 39 | if not et or not (et in inm or '*' in inm): return False 40 | 41 | if ims or inm: 42 | # @see: http://tools.ietf.org/html/rfc2616#section-10.3.5 43 | handler.error(304) 44 | del handler.response.headers['Last-Modified'] 45 | return True 46 | else: 47 | return False 48 | 49 | class webcached(object): 50 | ''' 51 | Decorator to enable conditional get. Add a lastModified method to the handler 52 | 53 | @param cacheCtrl: A string or a two-element tuple (CC for anonymous, CC for logged in user) 54 | ''' 55 | def __init__(self, cacheCtrl='no-cache', vary=None, genEtag=True): 56 | self.cacheCtrl = (cacheCtrl, cacheCtrl) if isinstance(cacheCtrl, basestring) else cacheCtrl 57 | self.vary = vary 58 | self.genEtag = genEtag 59 | 60 | def __call__(self, f): 61 | @wraps(f) 62 | def wrapped(handler, *args): 63 | handler.lastModified = MethodType(_lastModified, handler, handler.__class__) 64 | 65 | try: 66 | f(handler, *args) 67 | except _ResponseNotModified: 68 | self._setHeader(handler) 69 | return 70 | 71 | if handler.response.status == 200: 72 | self._setHeader(handler) 73 | if self.genEtag and handler.response.headers.get('ETag') is None: 74 | body = handler.response.out.getvalue() 75 | if isinstance(body, unicode): body = body.encode('utf-8') 76 | handler.response.headers['ETag'] = '"' + md5(body).hexdigest() + '"' 77 | _validate(handler) 78 | else: 79 | del handler.response.headers['Last-Modified'] 80 | del handler.response.headers['ETag'] 81 | 82 | return wrapped 83 | 84 | def _setHeader(self, handler): 85 | handler.response.headers['Cache-Control'] = self.cacheCtrl[1 if users.get_current_user() else 0] 86 | if self.vary: handler.response.headers['Vary'] = self.vary 87 | -------------------------------------------------------------------------------- /handlers/changelog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from google.appengine.ext import webapp 4 | from google.appengine.api import memcache 5 | from django.utils.feedgenerator import DefaultFeed as Feed 6 | 7 | from models import RuleList, ChangeLog 8 | from util import template, webcached 9 | 10 | def getSampleUrlFromRule(rule): 11 | from urllib import unquote 12 | rule = unquote(rule.encode()) 13 | try: 14 | rule = rule.decode('utf-8', 'strict') 15 | except UnicodeDecodeError: 16 | rule = rule.decode('gbk', 'ignore') 17 | if rule.startswith('||'): return 'http://' + rule[2:] 18 | if rule.startswith('.'): return 'http://' + rule[1:] 19 | if rule.startswith('|'): return rule[1:] 20 | rule = rule.replace('wikipedia.org*', 'wikipedia.org/wiki/') 21 | if not rule.startswith('http'): return 'http://' + rule 22 | return rule 23 | 24 | def generateLogFromDiff(diff): 25 | from collections import defaultdict 26 | urlStatus = defaultdict(lambda:{True:[], False:[]}) 27 | log = {'timestamp':diff.date, 'block':[], 'unblock':[], 'rule_adjust':[]} 28 | 29 | for type in ('add', 'remove'): 30 | blocked = type == 'add' 31 | for rule in getattr(diff, type): 32 | if rule.startswith('@@'): 33 | url = getSampleUrlFromRule(rule[2:]) 34 | log['rule_adjust'].append({'from':(), 'to':(rule,), 'sample_url':url}) 35 | else: 36 | url = getSampleUrlFromRule(rule) 37 | urlStatus[url][blocked].append(rule) 38 | 39 | for url, status in urlStatus.items(): 40 | if status[True] and not status[False]: 41 | log['block'].append({'rules':status[True], 'sample_url':url}) 42 | elif not status[True] and status[False]: 43 | log['unblock'].append({'rules':status[False], 'sample_url':url}) 44 | else: 45 | log['rule_adjust'].append({'from':status[False], 'to':status[True], 'sample_url':url}) 46 | 47 | return log 48 | 49 | class FeedHandler(webapp.RequestHandler): 50 | @webcached() 51 | def get(self, name): 52 | name = name.lower() 53 | rules = RuleList.getList(name) 54 | if rules is None: 55 | self.error(404) 56 | return 57 | 58 | # Conditional redirect to FeedBurner 59 | # @see: http://www.google.com/support/feedburner/bin/answer.py?hl=en&answer=78464 60 | if(self.request.get('raw', None) is None and # http://host/path/name.rss?raw 61 | 'FeedBurner' not in self.request.user_agent): # FeedBurner fetcher 62 | self.redirect('http://feeds.feedburner.com/%s' % name, permanent=False) 63 | return 64 | 65 | self.lastModified(rules.date) 66 | 67 | start = int(self.request.get('start', 0)) 68 | fetchNum = start + int(self.request.get('num', 20)) 69 | if fetchNum > 1000: 70 | self.error(412) 71 | return 72 | 73 | logs = memcache.get('changelog/%s' % name) 74 | if logs is None or len(logs) < fetchNum: 75 | diff = ChangeLog.gql("WHERE ruleList = :1 ORDER BY date DESC", rules).fetch(fetchNum) 76 | logs = map(generateLogFromDiff, diff) 77 | memcache.add('changelog/%s' % name, logs) 78 | 79 | self.response.headers['Content-Type'] = Feed.mime_type 80 | 81 | f = Feed(title="%s 更新记录" % name, 82 | link=self.request.relative_url(name), 83 | description="beta", 84 | language="zh") 85 | 86 | for item in logs: 87 | f.add_item(title="%d月%d日 %s 更新: 增加 %d 条, 删除 %d 条" % (item['timestamp'].month, item['timestamp'].day, name, len(item['block']), len(item['unblock'])), 88 | link='', 89 | description=template.render('changelogRssItem.html', **item), 90 | author_name="gfwlist", 91 | pubdate=item['timestamp']) 92 | 93 | f.write(self.response.out, 'utf-8') 94 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {%if is_dev%}(DEV){%endif%} 9 | {%block title%} 10 | {%block title1%}Untitled{%endblock%} - AutoProxy2PAC 11 | {%endblock%} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {%block css_and_script%}{%endblock%} 21 | 22 | 23 | 24 | 25 | 38 | 39 | 40 |
41 | 65 | 66 |
67 | {%block content%}BODY GOES HERE{%endblock%} 68 |
69 | 70 | 83 |
84 | 85 | {%block deferred_script%}{%endblock%} 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {%extends "2column.html"%} 2 | 3 | {%block title%}AutoProxy2PAC - 全平台智能代理{%endblock%} 4 | 5 | {%block css_and_script%} 6 | 7 | {%endblock%} 8 | 9 | {%block deferred_script2%} 10 | 11 | 61 | {%endblock%} 62 | 63 | {%block tagline%} 64 | 正常网站——直接访问
被墙站点——使用代理 65 | {%endblock%} 66 | 67 | {%block main%} 68 |
69 |
70 | 请选择你使用的代理工具 71 | {%for item in presetProxies%} 72 |

73 | {%endfor%} 74 |

75 | 76 | 80 | : 81 | 82 |

83 | 84 |
85 |
86 | 89 |
90 |

91 |

http://pac.autoproxy.org/{{pacUserUrlPrefix}}

92 |
93 |
94 |
95 | 或 96 | 下载PAC文件 97 |
98 | 99 |
100 | {%endblock%} 101 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Lucida Grande', Tahoma, Arial, 'Microsoft YaHei', STHeiTi, simsun, sans-serif; 3 | } 4 | 5 | a { 6 | color: #369; 7 | text-decoration: none; 8 | } 9 | 10 | a:visited { 11 | color: #669; 12 | } 13 | 14 | a:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | a img { 19 | border-style: none; 20 | } 21 | 22 | #page { 23 | width: 800px; 24 | margin: auto; 25 | } 26 | 27 | #header { 28 | display: block; 29 | position: relative; 30 | margin-bottom: 30px; 31 | } 32 | 33 | #header a { 34 | color: #444; 35 | text-decoration: none; 36 | } 37 | 38 | #logo { 39 | font-family: Georgia, 'Times New Roman', Times, serif; 40 | font-size: 40px; 41 | font-weight: normal; 42 | line-height: 1.4; 43 | border-bottom: 2px solid #444; 44 | margin: 0px; 45 | } 46 | 47 | #logo a { 48 | color: black; 49 | } 50 | 51 | #tagline { 52 | font-size: 14px; 53 | font-weight: normal; 54 | margin: 8px 0px 0px 0px; 55 | } 56 | 57 | #navigation { 58 | font-size: 18px; 59 | position: absolute; 60 | left: 320px; 61 | margin-top: 22px; 62 | } 63 | 64 | #navigation ul { 65 | list-style-type: none; 66 | margin: 0px; 67 | padding: 0px; 68 | } 69 | 70 | #navigation li { 71 | display: inline; 72 | margin: 0px 12px; 73 | } 74 | 75 | #userpanel { 76 | font-size: 14px; 77 | float: right; 78 | margin-top: 26px; 79 | } 80 | 81 | #footer { 82 | display: block; 83 | font-size: 12px; 84 | color: #777; 85 | text-align: center; 86 | border-top: 1px solid #ccc; 87 | margin-top: 20px; 88 | padding: 10px 0px 20px 0px; 89 | } 90 | 91 | #footer #poweredby { 92 | margin-left: 50px; 93 | } 94 | 95 | #footer #poweredby a { 96 | margin-left: 8px; 97 | } 98 | 99 | /* Two-column template */ 100 | #main { 101 | float: left; 102 | width: 480px; 103 | } 104 | 105 | #sidebar { 106 | font-size: 12px; 107 | float: right; 108 | width: 290px; 109 | } 110 | 111 | #sidebar h1 { 112 | font-size: 14px; 113 | } 114 | 115 | #changelog h1 a { 116 | text-decoration: none; 117 | } 118 | 119 | #changelog h1 img { 120 | vertical-align: top; 121 | } 122 | 123 | div.feedburnerFeedBlock { 124 | font-size: 11px; 125 | display: none; 126 | } 127 | 128 | div.feedburnerFeedBlock > ul { 129 | white-space: nowrap; 130 | list-style-type: none; 131 | padding: 0px; 132 | } 133 | 134 | div.feedburnerFeedBlock > ul > li { 135 | margin-top: 15px; 136 | } 137 | 138 | div.feedburnerFeedBlock span.headline { 139 | font-size: 12px; 140 | } 141 | 142 | div.feedburnerFeedBlock > ul > li > div { 143 | margin-top: 5px; 144 | } 145 | 146 | #creditfooter { 147 | display: none; 148 | } 149 | 150 | #tiptip_content p, #tiptip_content ul { 151 | margin: 3px 0px; 152 | } 153 | 154 | #tiptip_content ul { 155 | padding-left: 15px; 156 | } 157 | 158 | /* PAC configure page */ 159 | #config { 160 | font-size: 12px; 161 | background-color: #c8e8f3; 162 | padding: 30px 40px; 163 | width: 380px; 164 | margin: 0px auto; 165 | -moz-border-radius: 12px; /* FF1+ */ 166 | -webkit-border-radius: 12px; /* Saf3+, Chrome */ 167 | border-radius: 12px; /* Opera 10.5, IE 9 */ 168 | -moz-box-shadow: 0px 0px 6px #ccc; /* FF3.5+ */ 169 | -webkit-box-shadow: 0px 0px 6px #ccc; /* Saf3.0+, Chrome */ 170 | box-shadow: 0px 0px 6px #ccc; /* Opera 10.5, IE 9.0 */ 171 | } 172 | 173 | #config p { 174 | margin: 3px 0px; 175 | } 176 | 177 | #config fieldset { 178 | border: none; 179 | padding: 5px 0px; 180 | } 181 | 182 | #config legend { 183 | padding: 0px; 184 | } 185 | 186 | #config fieldset[name = proxy-select] { 187 | font-size: 16px; 188 | } 189 | 190 | #config fieldset[name = proxy-select] legend { 191 | width: 100%; 192 | border-bottom: 1px dashed #147; 193 | padding: 0px 5px 5px; 194 | margin: 0px -5px; 195 | } 196 | 197 | #proxy-input { 198 | font-size: 14px; 199 | margin-left: 15px; 200 | } 201 | 202 | #custom-pane { 203 | display: none; 204 | margin: 0px 12px; 205 | } 206 | 207 | #custom-pane label { 208 | color: #444; 209 | } 210 | 211 | #custom-pane textarea { 212 | font-family: monospace; 213 | width: 340px; 214 | overflow: auto; 215 | } 216 | 217 | #config button { 218 | background-color: #369; 219 | border: 1px solid #147; 220 | color: #eee; 221 | font-size: 15px; 222 | cursor: pointer; 223 | padding: 6px 10px; 224 | margin-left: 20px; 225 | -moz-border-radius: 6px; /* FF1+ */ 226 | -webkit-border-radius: 6px; /* Saf3+, Chrome */ 227 | border-radius: 6px; /* Opera 10.5, IE 9 */ 228 | } 229 | -------------------------------------------------------------------------------- /static/jquery.tipTip.minified.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TipTip 3 | * Copyright 2010 Drew Wilson 4 | * www.drewwilson.com 5 | * code.drewwilson.com/entry/tiptip-jquery-plugin 6 | * 7 | * Version 1.3 - Updated: Mar. 23, 2010 8 | * 9 | * This Plug-In will create a custom tooltip to replace the default 10 | * browser tooltip. It is extremely lightweight and very smart in 11 | * that it detects the edges of the browser window and will make sure 12 | * the tooltip stays within the current window size. As a result the 13 | * tooltip will adjust itself to be displayed above, below, to the left 14 | * or to the right depending on what is necessary to stay within the 15 | * browser window. It is completely customizable as well via CSS. 16 | * 17 | * This TipTip jQuery plug-in is dual licensed under the MIT and GPL licenses: 18 | * http://www.opensource.org/licenses/mit-license.php 19 | * http://www.gnu.org/licenses/gpl.html 20 | */ 21 | (function($){$.fn.tipTip=function(options){var defaults={activation:"hover",keepAlive:false,maxWidth:"200px",edgeOffset:3,defaultPosition:"bottom",delay:400,fadeIn:200,fadeOut:200,attribute:"title",content:false,enter:function(){},exit:function(){}};var opts=$.extend(defaults,options);if($("#tiptip_holder").length<=0){var tiptip_holder=$('
');var tiptip_content=$('
');var tiptip_arrow=$('
');$("body").append(tiptip_holder.html(tiptip_content).prepend(tiptip_arrow.html('
')))}else{var tiptip_holder=$("#tiptip_holder");var tiptip_content=$("#tiptip_content");var tiptip_arrow=$("#tiptip_arrow")}return this.each(function(){var org_elem=$(this);if(opts.content){var org_title=opts.content}else{var org_title=org_elem.attr(opts.attribute)}if(org_title!=""){if(!opts.content){org_elem.removeAttr(opts.attribute)}var timeout=false;if(opts.activation=="hover"){org_elem.hover(function(){active_tiptip()},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}else if(opts.activation=="focus"){org_elem.focus(function(){active_tiptip()}).blur(function(){deactive_tiptip()})}else if(opts.activation=="click"){org_elem.click(function(){active_tiptip()}).hover(function(){},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}function active_tiptip(){opts.enter.call(this);tiptip_content.html(org_title);tiptip_holder.hide().removeAttr("class").css("margin","0");tiptip_arrow.removeAttr("style");var top=parseInt(org_elem.offset()['top']);var left=parseInt(org_elem.offset()['left']);var org_width=parseInt(org_elem.outerWidth());var org_height=parseInt(org_elem.outerHeight());var tip_w=tiptip_holder.outerWidth();var tip_h=tiptip_holder.outerHeight();var w_compare=Math.round((org_width-tip_w)/2);var h_compare=Math.round((org_height-tip_h)/2);var marg_left=Math.round(left+w_compare);var marg_top=Math.round(top+org_height+opts.edgeOffset);var t_class="";var arrow_top="";var arrow_left=Math.round(tip_w-12)/2;if(opts.defaultPosition=="bottom"){t_class="_bottom"}else if(opts.defaultPosition=="top"){t_class="_top"}else if(opts.defaultPosition=="left"){t_class="_left"}else if(opts.defaultPosition=="right"){t_class="_right"}var right_compare=(w_compare+left)parseInt($(window).width());if((right_compare&&w_compare<0)||(t_class=="_right"&&!left_compare)||(t_class=="_left"&&left<(tip_w+opts.edgeOffset+5))){t_class="_right";arrow_top=Math.round(tip_h-13)/2;arrow_left=-12;marg_left=Math.round(left+org_width+opts.edgeOffset);marg_top=Math.round(top+h_compare)}else if((left_compare&&w_compare<0)||(t_class=="_left"&&!right_compare)){t_class="_left";arrow_top=Math.round(tip_h-13)/2;arrow_left=Math.round(tip_w);marg_left=Math.round(left-(tip_w+opts.edgeOffset+5));marg_top=Math.round(top+h_compare)}var top_compare=(top+org_height+opts.edgeOffset+tip_h+8)>parseInt($(window).height()+$(window).scrollTop());var bottom_compare=((top+org_height)-(opts.edgeOffset+tip_h+8))<0;if(top_compare||(t_class=="_bottom"&&top_compare)||(t_class=="_top"&&!bottom_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_top"}else{t_class=t_class+"_top"}arrow_top=tip_h;marg_top=Math.round(top-(tip_h+5+opts.edgeOffset))}else if(bottom_compare|(t_class=="_top"&&bottom_compare)||(t_class=="_bottom"&&!top_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_bottom"}else{t_class=t_class+"_bottom"}arrow_top=-12;marg_top=Math.round(top+org_height+opts.edgeOffset)}if(t_class=="_right_top"||t_class=="_left_top"){marg_top=marg_top+5}else if(t_class=="_right_bottom"||t_class=="_left_bottom"){marg_top=marg_top-5}if(t_class=="_left_top"||t_class=="_left_bottom"){marg_left=marg_left+5}tiptip_arrow.css({"margin-left":arrow_left+"px","margin-top":arrow_top+"px"});tiptip_holder.css({"margin-left":marg_left+"px","margin-top":marg_top+"px"}).attr("class","tip"+t_class);if(timeout){clearTimeout(timeout)}timeout=setTimeout(function(){tiptip_holder.stop(true,true).fadeIn(opts.fadeIn)},opts.delay)}function deactive_tiptip(){opts.exit.call(this);if(timeout){clearTimeout(timeout)}tiptip_holder.fadeOut(opts.fadeOut)}}})}})(jQuery); -------------------------------------------------------------------------------- /autoproxy2pac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | A tool to automatically download autoproxy's GFW list and convert it to a PAC file 6 | So you can bypass GFW's blockade on almost every browser 7 | 8 | @version: 0.1 9 | @requires: python 2.6 10 | 11 | @author: Meng Xiangliang @ 9#, Tsinghua University 12 | @contact: 911mxl gmail (e-mail), mengxl (twitter) 13 | 14 | @see: AutoProxy add-on for Firefox (https://addons.mozilla.org/en-US/firefox/addon/11009) 15 | 16 | @todo: 17 | - Read parameters from command-line 18 | - Generate PAC file using shExpMatch function instead of regular expression, should be faster, 19 | but it's already fast enough on Safari 4 20 | ''' 21 | 22 | from __future__ import with_statement 23 | import logging 24 | 25 | # Variable names in the PAC file 26 | proxyVar = "PROXY" 27 | defaultVar = "DEFAULT" 28 | 29 | # String constants 30 | rulesBegin = "//-- AUTO-GENERATED RULES, DO NOT MODIFY!" 31 | rulesEnd = "//-- END OF AUTO-GENERATED RULES" 32 | 33 | defaultPacTemplate = '''/* 34 | * Proxy Auto-Config file generated by autoproxy2pac 35 | * Rule source: %(ruleListUrl)s 36 | * Last update: %(ruleListDate)s 37 | */ 38 | function FindProxyForURL(url, host) { 39 | var %(proxyVar)s = "%(proxyString)s"; 40 | var %(defaultVar)s = "%(defaultString)s"; 41 | %(customCodePre)s 42 | %(rulesBegin)s 43 | %(ruleListCode)s 44 | %(rulesEnd)s 45 | %(customCodePost)s 46 | return %(defaultVar)s; 47 | } 48 | ''' 49 | 50 | def fetchRuleList(url): 51 | import urllib, base64 52 | from contextlib import closing 53 | with closing(urllib.urlopen(url)) as response: 54 | list = base64.decodestring(response.read()) 55 | date = response.info().getheader('last-modified') 56 | return list, date 57 | 58 | def rule2js(ruleList): 59 | import re 60 | jsCode = [] 61 | 62 | # The syntax of the list is based on Adblock Plus filter rules (http://adblockplus.org/en/filters) 63 | # Filter options (those parts start with "$") is not supported 64 | # AutoProxy Add-on for Firefox has a Javascript implementation 65 | # http://github.com/lovelywcm/autoproxy/blob/master/chrome/content/filterClasses.js 66 | for line in ruleList.splitlines()[1:]: 67 | # Ignore the first line ([AutoProxy x.x]), empty lines and comments 68 | if line and not line.startswith("!"): 69 | useProxy = True 70 | 71 | # Exceptions 72 | if line.startswith("@@"): 73 | line = line[2:] 74 | useProxy = False 75 | 76 | # Regular expressions 77 | if line.startswith("/") and line.endswith("/"): 78 | jsRegexp = line[1:-1] 79 | 80 | # Other cases 81 | else: 82 | # Remove multiple wildcards 83 | jsRegexp = re.sub(r"\*+", r"*", line) 84 | # Remove anchors following separator placeholder 85 | jsRegexp = re.sub(r"\^\|$", r"^", jsRegexp, 1) 86 | # Escape special symbols 87 | jsRegexp = re.sub(r"(\W)", r"\\\1", jsRegexp) 88 | # Replace wildcards by .* 89 | jsRegexp = re.sub(r"\\\*", r".*", jsRegexp) 90 | # Process separator placeholders 91 | jsRegexp = re.sub(r"\\\^", r"(?:[^\w\-.%\u0080-\uFFFF]|$)", jsRegexp) 92 | # Process extended anchor at expression start 93 | jsRegexp = re.sub(r"^\\\|\\\|", r"^[\w\-]+:\/+(?!\/)(?:[^\/]+\.)?", jsRegexp, 1) 94 | # Process anchor at expression start 95 | jsRegexp = re.sub(r"^\\\|", "^", jsRegexp, 1) 96 | # Process anchor at expression end 97 | jsRegexp = re.sub(r"\\\|$", "$", jsRegexp, 1) 98 | # Remove leading wildcards 99 | jsRegexp = re.sub(r"^(\.\*)", "", jsRegexp, 1) 100 | # Remove trailing wildcards 101 | jsRegexp = re.sub(r"(\.\*)$", "", jsRegexp, 1) 102 | 103 | if jsRegexp == "": 104 | jsRegexp = ".*" 105 | logging.warning("There is one rule that matches all URL, which is highly *NOT* recommended: %s", line) 106 | 107 | jsLine = " if(/%s/i.test(url)) return %s;" % (jsRegexp, proxyVar if useProxy else defaultVar) 108 | if useProxy: 109 | jsCode.append(jsLine) 110 | else: 111 | jsCode.insert(0, jsLine) 112 | 113 | return '\n'.join(jsCode) 114 | 115 | def parseTemplate(content): 116 | import re 117 | template, n = re.subn(r'(?ms)^(\s*?%s\s*?)^.*$(\s*?%s\s*?)$' % (re.escape(rulesBegin), re.escape(rulesEnd)), r'\1%(ruleListCode)s\2', content) 118 | if n == 0: 119 | logging.warning("Can not find auto-generated rule section, user-defined rules will LOST during the update") 120 | return defaultPacTemplate 121 | 122 | template = re.sub(r'(Rule source: ).+', r'\1%(ruleListUrl)s', template) 123 | template = re.sub(r'(Last update: ).+', r'\1%(ruleListDate)s', template) 124 | return template 125 | 126 | def generatePac(rules, configs, template=defaultPacTemplate): 127 | data = { 'proxyVar' : proxyVar, 128 | 'defaultVar' : defaultVar, 129 | 'rulesBegin' : rulesBegin, 130 | 'rulesEnd' : rulesEnd, 131 | 'customCodePre' : '', 132 | 'customCodePost' : '', 133 | } 134 | data.update(configs) 135 | data.update(rules) 136 | return template % data 137 | 138 | if __name__ == '__main__': 139 | pacFilepath = "fuckgfw.pac" 140 | ruleListUrl = "http://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt" 141 | proxyString = "PROXY 127.0.0.1:8118" 142 | defaultString = "DIRECT" 143 | 144 | print("Fetching GFW list from %s ..." % ruleListUrl) 145 | ruleList, ruleListDate = fetchRuleList(ruleListUrl) 146 | 147 | try: 148 | # Try to update the old PAC file 149 | with open(pacFilepath) as f: 150 | template = parseTemplate(f.read()) 151 | print("Updating %s ..." % pacFilepath) 152 | 153 | except IOError: 154 | # Generate new PAC file 155 | template = defaultPacTemplate 156 | print("Generating %s ..." % pacFilepath) 157 | 158 | rules = { 'ruleListUrl' : ruleListUrl, 159 | 'ruleListDate' : ruleListDate, 160 | 'ruleListCode' : rule2js(ruleList) } 161 | configs = { 'proxyString' : proxyString, 162 | 'defaultString' : defaultString } 163 | with open(pacFilepath, 'w') as f: 164 | f.write(generatePac(rules, configs, template)) 165 | -------------------------------------------------------------------------------- /handlers/pac_generate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import re 5 | from base64 import urlsafe_b64decode, urlsafe_b64encode 6 | from calendar import timegm 7 | from datetime import datetime 8 | from email.utils import formatdate, parsedate 9 | from urllib import unquote 10 | from google.appengine.ext import webapp 11 | from google.appengine.api import memcache 12 | 13 | import autoproxy2pac 14 | from models import RuleList, UserSetting 15 | from util import useragent, webcached 16 | from settings import DEBUG, MAIN_SERVER, PRESET_PROXIES, MIRRORS, RATELIMIT_ENABLED, RATELIMIT_DURATION, RATELIMIT_QUOTA, MAX_CUSTOM_RULE_NUMBER_FOR_MIRROR, \ 17 | PAC_USER_URL_PREFIX 18 | 19 | privoxyConfCode = ''' 20 | if(host == "p.p" || dnsDomainIs(host, "config.privoxy.org")) return PROXY; 21 | ''' 22 | 23 | class Handler(webapp.RequestHandler): 24 | @webcached('public,max-age=600') # 10min 25 | def get(self, urlpart): 26 | download = self.request.get('download', None) is not None 27 | 28 | # Redirect to usage page for visits from links (obviously not a browser PAC fetcher) 29 | if MAIN_SERVER and not download and 'Referer' in self.request.headers: 30 | self.redirect("/usage?u=" + urlpart, permanent=False) 31 | return 32 | 33 | if not self.parseRequest(urlpart): 34 | self.error(404) 35 | return 36 | 37 | rules = RuleList.getList('gfwlist') 38 | if rules is None: 39 | self.error(500) 40 | return 41 | 42 | pacTime = formatdate(timegm(max(self.settingTime, datetime(*parsedate(rules.date)[:6])).timetuple()), False, True) 43 | self.response.headers['ETag'] = '"' + pacTime.replace(',', '').replace(' ', '') + '"' 44 | self.lastModified(pacTime) 45 | 46 | # Load balance 47 | if MAIN_SERVER and len(self.customRules) <= MAX_CUSTOM_RULE_NUMBER_FOR_MIRROR: 48 | mirror = self.pickMirror() 49 | if mirror: 50 | query = ['e=' + urlsafe_b64encode(r) for r in self.customRules] 51 | if download: query.append('download') 52 | mirror = '%s/%s?%s' % (mirror, self.proxyDict['urlpart'], '&'.join(query)) 53 | logging.debug('Redirect the PAC fetcher to %s', mirror) 54 | if not DEBUG: 55 | # A fixed server for a rate-limiting cycle 56 | self.response.headers['Cache-Control'] = 'public,max-age=%d' % (RATELIMIT_DURATION * 3600) 57 | self.redirect(mirror, permanent=False) 58 | return 59 | 60 | if RATELIMIT_ENABLED and self.isRateLimited(): return 61 | 62 | customJs = autoproxy2pac.rule2js('\n'.join([''] + self.customRules)) 63 | if self.proxyDict['name'] == 'privoxy': customJs = privoxyConfCode + customJs 64 | configs = { 65 | 'proxyString': self.proxyString, 66 | 'defaultString': 'DIRECT', 67 | 'customCodePre': customJs, 68 | } 69 | pac = autoproxy2pac.generatePac(rules.toDict(), configs, autoproxy2pac.defaultPacTemplate) 70 | import base64 71 | pac = '''function decode64(_1){var _2="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var _3="";var _4,_5,_6;var _7,_8,_9,_a;var i=0;_1=_1.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{_7=_2.indexOf(_1.charAt(i++));_8=_2.indexOf(_1.charAt(i++));_9=_2.indexOf(_1.charAt(i++));_a=_2.indexOf(_1.charAt(i++));_4=(_7<<2)|(_8>>4);_5=((_8&15)<<4)|(_9>>2);_6=((_9&3)<<6)|_a;_3=_3+String.fromCharCode(_4);if(_9!=64){_3=_3+String.fromCharCode(_5);}if(_a!=64){_3=_3+String.fromCharCode(_6);}}while(i<_1.length);return _3;}eval(decode64("%s"))''' % base64.b64encode(pac) 72 | 73 | self.response.headers['Content-Type'] = 'application/x-ns-proxy-autoconfig' 74 | if download: self.response.headers['Content-Disposition'] = 'attachment; filename="autoproxy.pac"' 75 | self.response.out.write(pac) 76 | 77 | userPacRegxp = re.compile(r'^%s([^/\s]+)(?:/(.+))?$' % PAC_USER_URL_PREFIX) 78 | proxyRegxp = re.compile(r'''^(?P 79 | (?P [^/\s]+) | 80 | (?P proxy|http|socks) / (?P [^/\s]+) / (?P \d+) 81 | )$''', re.VERBOSE) 82 | 83 | def parseRequest(self, urlpart): 84 | self.customRules = self.request.get_all('c') 85 | self.customRules += (urlsafe_b64decode(r.encode('ascii')) for r in self.request.get_all('e')) 86 | 87 | match = self.userPacRegxp.match(unquote(urlpart).strip()) 88 | if match: 89 | setting = UserSetting.gql('WHERE pacName=:1', match.group(1).lower()).get() 90 | if setting is None: return 91 | 92 | urlpart = match.group(2) or setting.defaultProxy 93 | self.customRules += setting.customRules 94 | self.settingTime = setting.lastModified 95 | else: 96 | self.settingTime = datetime.min 97 | 98 | match = self.proxyRegxp.match(urlpart.lower()) 99 | if match is None: return 100 | self.proxyDict = match.groupdict() 101 | 102 | if self.proxyDict['name']: 103 | if self.proxyDict['name'] not in PRESET_PROXIES: return 104 | self.proxyString = PRESET_PROXIES[self.proxyDict['name']][1] 105 | elif self.proxyDict['type']: 106 | self.proxyDict['type'] = 'SOCKS' if self.proxyDict['type'] == 'socks' else 'PROXY' 107 | self.proxyString = '%(type)s %(host)s:%(port)s' % self.proxyDict 108 | 109 | # Chrome expects 'SOCKS5' instead of 'SOCKS', see http://j.mp/pac-test 110 | if useragent.family() == 'Chrome': 111 | self.proxyString = self.proxyString.replace('SOCKS ', 'SOCKS5 ') 112 | 113 | return True 114 | 115 | def pickMirror(self): 116 | return MIRRORS[hash(self.request.remote_addr) % len(MIRRORS)] 117 | 118 | def isRateLimited(self): 119 | param = {'ip':self.request.remote_addr, 'ua':self.request.user_agent} 120 | 121 | key = '%(ua)s@%(ip)s' % param 122 | rate = memcache.incr(key, namespace='rate') # incr won't refresh the expiration time 123 | if rate is None: 124 | rate = 1 125 | memcache.add(key, 1, RATELIMIT_DURATION * 3600, namespace='rate') 126 | 127 | quota = RATELIMIT_QUOTA(**param) 128 | if rate > quota: 129 | if rate == quota + 1: 130 | logging.info('%(ip)s has reached the rate limit (%(qt)d per %(dur)dh), UA="%(ua)s"', dict(qt=quota, dur=RATELIMIT_DURATION, **param)) 131 | logging.debug('%(ip)s is banned on full fetch #%(rt)d, UA="%(ua)s"', dict(rt=rate, **param)) 132 | if not DEBUG: 133 | self.error(403) 134 | return True 135 | 136 | return False 137 | --------------------------------------------------------------------------------