├── .gitignore ├── auth.htpasswd.sample ├── nginx.conf ├── readme.md ├── scripts ├── include │ └── common.lua ├── proxy.lua ├── redirect.lua └── shorten.lua ├── static ├── main.css └── shadowshorten_inject.js └── template └── proxy.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | #!! ERROR: nginx is undefined. Use list command to see defined gitignore types !!# 4 | # Created by https://www.gitignore.io 5 | 6 | ### Lua ### 7 | # Compiled Lua sources 8 | luac.out 9 | 10 | # luarocks build files 11 | *.src.rock 12 | *.zip 13 | *.tar.gz 14 | 15 | # Object files 16 | *.o 17 | *.os 18 | *.ko 19 | *.obj 20 | *.elf 21 | 22 | # Precompiled Headers 23 | *.gch 24 | *.pch 25 | 26 | # Libraries 27 | *.lib 28 | *.a 29 | *.la 30 | *.lo 31 | *.def 32 | *.exp 33 | 34 | # Shared objects (inc. Windows DLLs) 35 | *.dll 36 | *.so 37 | *.so.* 38 | *.dylib 39 | 40 | # Executables 41 | *.exe 42 | *.out 43 | *.app 44 | *.i*86 45 | *.x86_64 46 | *.hex 47 | 48 | logs/ 49 | auth.htpasswd 50 | -------------------------------------------------------------------------------- /auth.htpasswd.sample: -------------------------------------------------------------------------------- 1 | # Generate by `openssl passwd` 2 | blahgeek:d4E07VPNg088A 3 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # This proxy is needed because lua-http module does not support https yet 3 | # see `$block_detect` below, also `scripts/shorten.lua` 4 | listen 80; 5 | listen [::]:80; 6 | server_name wdetect.blahgeek.com; 7 | 8 | location / { 9 | proxy_pass https://d3s2tsdr8lh01d.cloudfront.net; 10 | } 11 | } 12 | 13 | server { 14 | listen 80; 15 | listen [::]:80; 16 | server_name blaa.cf; 17 | 18 | access_log logs/ShadowShorten/access.log main; 19 | error_log logs/ShadowShorten/error.log; 20 | 21 | set $template_root /etc/nginx/apps/ShadowShorten/template/; 22 | 23 | set $block_detect http://wdetect.blahgeek.com; 24 | set $proxy_domain -ss.blahgeek.com; 25 | set $proxy_schema https; 26 | 27 | set $random_key_len 8; 28 | 29 | location = /new { 30 | auth_basic "Auth required"; 31 | auth_basic_user_file apps/ShadowShorten/auth.htpasswd; 32 | 33 | default_type text/html; 34 | content_by_lua_file apps/ShadowShorten/scripts/shorten.lua; 35 | } 36 | 37 | location ~ ^/([0-9a-zA-Z]+)$ { 38 | default_type text/html; 39 | content_by_lua_file apps/ShadowShorten/scripts/redirect.lua; 40 | } 41 | 42 | location /static { 43 | root apps/ShadowShorten; 44 | } 45 | } 46 | 47 | server { 48 | listen 80; 49 | listen [::]:80; 50 | server_name ~^(?[0-9a-z]+)-ss\.blahgeek\.com; 51 | 52 | access_log logs/ShadowShorten/access.log main; 53 | error_log logs/ShadowShorten/error.log; 54 | 55 | rewrite ^(.*) https://$host$1 redirect; 56 | } 57 | 58 | server { 59 | listen 443 ssl spdy; 60 | listen [::]:443 ssl spdy; 61 | 62 | access_log logs/ShadowShorten/access.log main; 63 | error_log logs/ShadowShorten/error.log; 64 | 65 | server_name ~^(?[0-9a-z]+)-ss\.blahgeek\.com; 66 | 67 | location / { 68 | set $custom_proxy_host ''; 69 | access_by_lua_file apps/ShadowShorten/scripts/proxy.lua; 70 | proxy_pass $custom_proxy_host; 71 | proxy_redirect $custom_proxy_host/ /; 72 | 73 | sub_filter ''; 74 | sub_filter_once on; 75 | 76 | proxy_set_header Accept-Encoding ""; # Do not accept g-zip 77 | } 78 | 79 | location /shadowshorten_inject.js { 80 | root apps/ShadowShorten/static; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ShadowShorten 2 | 3 | - It's an URL shortener. 4 | - It's a HTTP proxy for chinese users. 5 | - It uses lua, nginx and redis (openresty) 6 | 7 | [中文介绍](http://blog.blahgeek.com/ShadowShorten/) 8 | 9 | [Demo](http://blaa.cf/4djtgp6c) (Visit it from chinese, otherwise it's just a shorten URL, nothing more) 10 | 11 | ## How it works 12 | 13 | When it's going to shorten an URL, it checks if this website is blocked in china (via greatfire.org). If it is, it will provide a proxy to chinese user who visit it. 14 | 15 | ## How to run 16 | 17 | - Install openresty with some extra module: `--with-http_spdy_module --with-http_sub_module --with-http_geoip_module` 18 | - Install some extra lua lib (and set correct `lua_package_path` in your main `nginx.conf`): 19 | - [lua-resty-template](https://github.com/bungle/lua-resty-template) 20 | - [lua-resty-http](https://github.com/pintsized/lua-resty-http) 21 | - Add `geoip_country GeoIP.dat;` to your main `nginx.conf` (download it from somewhere) 22 | - (Optional) Get a wildcard SSL certification for you proxy domain and configure it in your main `nginx.conf` 23 | - Link `ShadowShorten` directory to `/etc/nginx/apps/` 24 | - Add `include /etc/nginx/apps/ShadowShorten/nginx.conf` to your main `nginx.conf` 25 | - Copy `auth.htpasswd.sample` to `auth.htpasswd` and change the password 26 | - Change `ShadowShorten/nginx.conf` as you need (domain names etc.) 27 | - Run nginx (openresty) 28 | 29 | ## How to use 30 | 31 | `http --form --auth user:passwd http://blaa.cf/new url="http://baidu.com"`(httpie) 32 | -------------------------------------------------------------------------------- /scripts/include/common.lua: -------------------------------------------------------------------------------- 1 | -- @Author: BlahGeek 2 | -- @Date: 2015-05-26 3 | -- @Last Modified by: BlahGeek 4 | -- @Last Modified time: 2015-05-26 5 | 6 | local redis = require "resty.redis" 7 | 8 | local _M = {} 9 | 10 | 11 | local exit = function(status, msg) 12 | ngx.status = status 13 | if msg then ngx.say(msg) end 14 | return ngx.exit(status) 15 | end 16 | 17 | _M.exit = exit 18 | 19 | function _M.new_redis(host, port) 20 | if host == nil then host = "127.0.0.1" end 21 | if port == nil then port = 6379 end 22 | local red = redis:new() 23 | red:set_timeout(1000) 24 | local ok, err = red:connect(host, port) 25 | if not ok then 26 | return exit(ngx.HTTP_INTERNAL_SERVER_ERROR, "Failed to connect to redis") 27 | end 28 | return red 29 | end 30 | 31 | return _M 32 | -------------------------------------------------------------------------------- /scripts/proxy.lua: -------------------------------------------------------------------------------- 1 | -- @Author: BlahGeek 2 | -- @Date: 2015-05-26 3 | -- @Last Modified by: BlahGeek 4 | -- @Last Modified time: 2015-05-26 5 | 6 | local common = require "ShadowShorten.scripts.include.common" 7 | 8 | local key = ngx.var.key; 9 | local red = common.new_redis() 10 | 11 | local res, err = red:hmget("shorten:" .. key, "host", "uri", "blocked") 12 | local host, uri, blocked = unpack(res) 13 | if host == ngx.null then 14 | return common.exit(ngx.HTTP_NOT_FOUND) 15 | end 16 | 17 | red:set_keepalive(10000, 10) 18 | 19 | if blocked == "false" then -- only forbidden if not blocked 20 | return common.exit(ngx.HTTP_FORBIDDEN) 21 | else 22 | ngx.var.custom_proxy_host = host 23 | return 24 | end 25 | -------------------------------------------------------------------------------- /scripts/redirect.lua: -------------------------------------------------------------------------------- 1 | -- @Author: BlahGeek 2 | -- @Date: 2015-05-26 3 | -- @Last Modified by: BlahGeek 4 | -- @Last Modified time: 2015-05-26 5 | 6 | local template = require "resty.template" 7 | local common = require "ShadowShorten.scripts.include.common" 8 | 9 | local key = ngx.var[1] 10 | local country = ngx.var.geoip_country_code 11 | 12 | local red = common.new_redis() 13 | 14 | local res, err = red:hmget("shorten:" .. key, "host", "uri", "blocked") 15 | local host, uri, blocked = unpack(res) 16 | if host == ngx.null then 17 | return common.exit(ngx.HTTP_NOT_FOUND) 18 | end 19 | 20 | red:set_keepalive(10000, 10) 21 | 22 | if country == "CN" and blocked ~= "false" then 23 | return template.render("proxy.html", { 24 | domain = host, 25 | url = host .. uri, 26 | proxy = ngx.var.proxy_schema .. "://" .. key .. ngx.var.proxy_domain .. uri 27 | }) 28 | else 29 | return ngx.redirect(host .. uri) 30 | end 31 | -------------------------------------------------------------------------------- /scripts/shorten.lua: -------------------------------------------------------------------------------- 1 | -- @Author: BlahGeek 2 | -- @Date: 2015-05-26 3 | -- @Last Modified by: BlahGeek 4 | -- @Last Modified time: 2015-05-30 5 | 6 | local random = require "resty.random" 7 | local http = require "resty.http" 8 | 9 | local common = require "ShadowShorten.scripts.include.common" 10 | 11 | local gen_random = function(len) 12 | local DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz" 13 | local DIGITS_LEN = 36 -- 10 + 26 14 | 15 | local random_raw_str = random.bytes(len) 16 | local random_str = '' 17 | for i = 1, len do 18 | local code = string.byte(random_raw_str, i) 19 | code = code % DIGITS_LEN + 1 20 | random_str = random_str .. string.sub(DIGITS, code, code) 21 | end 22 | 23 | return random_str 24 | end 25 | 26 | local is_block = function(url) 27 | -- return: true for blocked, false for not blocked, nil for unknown 28 | -- url must start with "http://" or "https://" 29 | local httpc = http.new() 30 | local req_url = string.format("%s/?type=gf_this_site&language=en-us&v=3&location=%s", 31 | ngx.var.block_detect, ngx.escape_uri(url)) 32 | local res, err = httpc:request_uri(req_url) 33 | if not res then return nil end 34 | 35 | local blocked = nil 36 | if string.find(res.body, "is not blocked in China") then 37 | blocked = false 38 | elseif string.find(res.body, "% blocked in China") then 39 | blocked = true 40 | end 41 | return blocked -- maybe nil 42 | end 43 | 44 | ----------------------------------------- 45 | -- Main scripts starts here 46 | ----------------------------------------- 47 | 48 | ngx.req.read_body() 49 | local args, err = ngx.req.get_post_args() 50 | 51 | local url = args and args["url"] 52 | if not url then return common.exit(ngx.HTTP_BAD_REQUEST) end 53 | 54 | local ttl = args and args["ttl"] 55 | 56 | if not string.find(url, "http://") and not string.find(url, "https://") then 57 | url = "http://" .. url 58 | end 59 | 60 | local url_scheme, url_host, url_port, url_path = unpack(http:parse_uri(url)) 61 | local url_scheme_host = url_scheme .. "://" .. url_host .. ":" .. tostring(url_port) 62 | if (url_port == 80 and url_scheme == "http") or (url_port == 443 and url_sceme == "https") then 63 | url_scheme_host = url_scheme .. "://" .. url_host 64 | end 65 | if url_path == nil or url_path == "" then url_path = "/" end 66 | 67 | local blocked = is_block(url_scheme_host) 68 | local key = gen_random(tonumber(ngx.var.random_key_len)) 69 | 70 | ---------------------------------------- 71 | -- Insert it into redis 72 | ---------------------------------------- 73 | 74 | local red = common.new_redis() 75 | local ok, err = red:hmset("shorten:" .. key, { 76 | host = url_scheme_host, 77 | uri = url_path, 78 | blocked = tostring(blocked) 79 | }) 80 | if not ok then 81 | return common.exit(ngx.HTTP_INTERNAL_SERVER_ERROR, "Failed to insert into redis") 82 | end 83 | 84 | if ttl then red:expire("shorten" .. key, ttl) end 85 | 86 | ngx.say(key) 87 | 88 | red:set_keepalive(10000, 10) 89 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | -ms-text-size-adjust: 100%; 4 | -webkit-text-size-adjust: 100%; 5 | } 6 | body { 7 | margin: 0; 8 | } 9 | a { 10 | text-decoration: inherit; 11 | color: inherit; 12 | } 13 | a:hover { 14 | text-decoration: underline; 15 | } 16 | .wrapper { 17 | margin: 65px auto 10px; 18 | } 19 | .btn { 20 | margin: 0 auto 5px; 21 | width: 120px; 22 | display: block; 23 | font-weight: normal; 24 | text-align: center; 25 | vertical-align: middle; 26 | -ms-touch-action: manipulation; 27 | touch-action: manipulation; 28 | cursor: pointer; 29 | background-image: none; 30 | border: 1px solid transparent; 31 | white-space: nowrap; 32 | padding: 6px 12px; 33 | font-size: 14px; 34 | line-height: 1.42857143; 35 | border-radius: 4px; 36 | color: #ffffff; 37 | background-color: #337ab7; 38 | border-color: #2e6da4; 39 | } 40 | .btn.white-btn { 41 | color: #333; 42 | background-color: #fff; 43 | border-color: #ccc 44 | } 45 | .btn:focus, 46 | .btn:active:focus { 47 | outline: thin dotted; 48 | outline: 5px auto -webkit-focus-ring-color; 49 | outline-offset: -2px; 50 | } 51 | .btn:hover, 52 | .btn:focus{ 53 | color: #ffffff; 54 | text-decoration: none; 55 | background-color: #286090; 56 | border-color: #204d74; 57 | } 58 | .btn.white-btn:hover, .btn.white-btn:focus{ 59 | color: #333; 60 | background-color: #e6e6e6; 61 | border-color: #adadad 62 | } 63 | .btn:active{ 64 | outline: 0; 65 | background-image: none; 66 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 67 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 68 | } 69 | 70 | .info { 71 | font-size: 14px; 72 | line-height: 20px; 73 | font-weight: 200; 74 | /*color: #babec2;*/ 75 | color: #6a6f75; 76 | text-align: center; 77 | margin: 25px auto 30px; 78 | max-width: 80%; 79 | } 80 | 81 | .title { 82 | white-space: nowrap; 83 | font-size: 20px; 84 | line-height: 24px; 85 | font-weight: 300; 86 | /*color: #6a6f75;*/ 87 | color: #babec2; 88 | text-align: center; 89 | margin: 4px auto 0px; 90 | max-width: 90%; 91 | } 92 | .title.small { 93 | font-size: 16px; 94 | line-height: 16px; 95 | margin-top: 0px; 96 | } 97 | -------------------------------------------------------------------------------- /static/shadowshorten_inject.js: -------------------------------------------------------------------------------- 1 | var shadowshorten_inject = '
' 2 | + '

Proxy by ShadowShorten

' 3 | + '

来自BlahGeek的科学短网址服务

' 4 | + '
'; 5 | document.write(shadowshorten_inject); 6 | -------------------------------------------------------------------------------- /template/proxy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ShadowShorten 8 | 9 | 10 |
11 |

ShadowShorten

12 |

来自BlahGeek的科学短网址服务

13 |

你正在使用中国大陆IP前往{{ domain }},可能导致无法访问
14 | 你可以选择使用ShadowShorten自带的代理继续访问

15 | 使用代理 16 | 直接访问 17 |
18 | 19 | 20 | --------------------------------------------------------------------------------