├── 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 %}
3 | {{ item.sample_url|urlize }}
4 | ({% for r in item.rules %}{{r}}{% if not forloop.last %}, {% endif %}{% endfor %})
5 | {% if forloop.last %} {% endif %}
6 | {% endfor %}
7 |
8 | {% for item in block %}
9 | {% if forloop.first %}封锁网址 {% endif %}
10 | {{ item.sample_url|urlize }}
11 | ({% for r in item.rules %}{{r}}{% if not forloop.last %}, {% endif %}{% endfor %})
12 | {% if forloop.last %} {% endif %}
13 | {% endfor %}
14 |
15 | {% for item in rule_adjust %}
16 | {% if forloop.first %}规则调整 {% endif %}
17 | {{ item.sample_url|urlize }}
18 | ({% for r in item.from %}{{r}}{% if not forloop.last %}, {% endif %}{% endfor %} →
19 | {% for r in item.to %}{{r}}{% if not forloop.last %}, {% endif %}{% endfor %})
20 | {% if forloop.last %} {% 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 |
76 |
77 |
78 |
79 |
80 |
81 |
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 |
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 |
--------------------------------------------------------------------------------