├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── auth.go ├── auth_test.go ├── chinaip_data.go ├── chinaip_gen.go ├── chinaip_init.go ├── chinaip_init_test.go ├── config.go ├── config_test.go ├── config_unix.go ├── config_windows.go ├── conn_pool.go ├── conn_pool_test.go ├── directip.go ├── directip_test.go ├── directlist.go ├── directlist_test.go ├── doc ├── implementation.md ├── osx │ └── net.ohrz.meow.plist └── sample-config │ ├── check_direct.py │ ├── direct │ ├── proxy │ ├── rc │ ├── rc-full │ └── reject ├── error.go ├── http.go ├── http_test.go ├── install.sh ├── log.go ├── main.go ├── pac.go ├── pac.js ├── parent_proxy.go ├── proxy.go ├── proxy_test.go ├── proxy_unix.go ├── proxy_windows.go ├── script ├── MEOW.ico ├── MEOW.png ├── README.md ├── build.sh ├── debugrc ├── httprc ├── log-group-by-client.sh ├── meow ├── meow-taskbar.exe ├── set-version.sh ├── test.sh └── upload.sh ├── ssh.go ├── stat.go ├── testdata └── file ├── timeoutset.go ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | *.sublime* 3 | meow-proxy 4 | MEOW 5 | MEOW.exe 6 | direct.txt 7 | rc.txt 8 | rc-full.txt 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | env: 5 | - TRAVIS="yes" 6 | install: 7 | - go get github.com/shadowsocks/shadowsocks-go/shadowsocks 8 | - go get github.com/cyfdecyf/bufio 9 | - go get github.com/cyfdecyf/leakybuf 10 | - go get github.com/cyfdecyf/color 11 | script: 12 | - pushd $TRAVIS_BUILD_DIR 13 | - go test -v 14 | - ./script/test.sh 15 | - popd 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新说明 2 | - 2016-09-29 Version 1.5 3 | 4 | * 更新中国IP列表 5 | 6 | - 2016-02-18 Version 1.3.4 7 | 8 | * 使用 Go 1.6 编译,请重新下载 9 | 10 | - 2015-12-03 Version 1.3.4 11 | 12 | * 修正客户端连接未正确关闭 bug 13 | * 修正对文件描述符过多错误的判断(too many open files) 14 | 15 | - 2015-11-22 Version 1.3.3 16 | 17 | * 增加 `reject` 拒绝连接列表 18 | * 支持作为 HTTPS 代理服务器监听 19 | * 支持 HTTPS 代理服务器作为父代理 20 | 21 | 22 | - 2015-10-09 Version 1.3.2 23 | 24 | * 完全托管在 github,不再使用 meowproxy.me 域名,[新的下载地址](https://github.com/renzhn/MEOW/tree/gh-pages/dist/) 25 | 26 | - 2015-08-23 Version 1.3.1 27 | 28 | * 去除了端口限制 29 | * 使用最新的 Go 1.5 编译 30 | 31 | - 2015-07-16 Version 1.3 32 | 33 | 更新了默认的直连列表、加入了强制使用代理列表,强烈推荐旧版本用户更新 [direct](https://raw.githubusercontent.com/renzhn/MEOW/master/doc/sample-config/direct) 文件和下载 [proxy](https://raw.githubusercontent.com/renzhn/MEOW/master/doc/sample-config/proxy) 文件(或者重新安装) 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | 3 | RUN apt-get update -y && apt-get install --no-install-recommends -y -q \ 4 | curl \ 5 | zip \ 6 | build-essential \ 7 | ca-certificates \ 8 | git mercurial bzr \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | ENV GOVERSION 1.6.2 12 | RUN mkdir /goroot && mkdir /gopath 13 | RUN curl https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz \ 14 | | tar xvzf - -C /goroot --strip-components=1 15 | 16 | ENV GOPATH /gopath 17 | ENV GOROOT /goroot 18 | ENV PATH $GOROOT/bin:$GOPATH/bin:$PATH 19 | 20 | RUN go get github.com/mitchellh/gox 21 | 22 | CMD go get -d ./... && gox 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Chen Yufei. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MEOW Proxy 2 | 3 | 当前版本:1.5 [CHANGELOG](CHANGELOG.md) 4 | [![Build Status](https://travis-ci.org/netheril96/MEOW.png?branch=master)](https://travis-ci.org/netheril96/MEOW) 5 | 6 |
 7 |        /\
 8 |    )  ( ')     MEOW 是 [COW](https://github.com/cyfdecyf/cow) 的一个派生版本
 9 |   (  /  )      MEOW 与 COW 最大的不同之处在于,COW 采用黑名单模式, 而 MEOW 采用白名单模式
10 |    \(__)|      国内网站直接连接,其他的网站使用代理连接
11 | 
12 | 13 | ## 与原版MEOW的差别 14 | 15 | * 本代码仓库删除了编译好的二进制文件,大大减少了git clone时的传输大小 16 | * IPv6一律走直连(对于教育网用户很有用) 17 | 18 | ## MEOW 可以用来 19 | - 作为全局 HTTP 代理(支持 PAC),可以智能分流(直连国内网站、使用代理连接其他网站) 20 | - 将 SOCKS5 等代理转换为 HTTP 代理,HTTP 代理能最大程度兼容各种软件(可以设置为程序代理)和设备(设置为系统全局代理) 21 | - 架设在内网(或者公网),为其他设备提供智能分流代理 22 | - 编译成一个无需任何依赖的可执行文件运行,支持各种平台(Win / Linux / OS X),甚至是树莓派(Linux ARM) 23 | 24 | ## 获取 25 | 26 | - **从源码安装:** 安装 [Go](http://golang.org/doc/install),然后 `go get github.com/netheril96/MEOW` 27 | 28 | ## 配置 29 | 30 | 编辑 `~/.meow/rc` (OS X, Linux) 或 `rc.txt` (Windows),例子: 31 | 32 | # 监听地址,设为0.0.0.0可以监听所有端口,共享给局域网使用 33 | listen = http://127.0.0.1:4411 34 | # 至少指定一个上级代理 35 | # SOCKS5 上级代理 36 | # proxy = socks5://127.0.0.1:1080 37 | # HTTP 上级代理 38 | # proxy = http://127.0.0.1:8087 39 | # shadowsocks 上级代理 40 | # proxy = ss://aes-128-cfb:password@example.server.com:25 41 | # HTTPS 上级代理 42 | # proxy = https://user:password@example.server.com:port 43 | 44 | ## 工作方式 45 | 46 | 当 MEOW 启动时会从配置文件加载直连列表和强制使用代理列表,详见下面两节。 47 | 48 | 当通过 MEOW 访问一个网站时,MEOW 会: 49 | 50 | - 检查域名是否在直连列表中,如果在则直连 51 | - 检查域名是否在强制使用代理列表中,如果在则通过代理连接 52 | - **检查域名的 IP 是否为国内 IP** 53 | - 通过本地 DNS 解析域名,得到域名的 IP 54 | - 如果是国内 IP 则直连,否则通过代理连接 55 | - 将域名加入临时的直连或者强制使用代理列表,下次可以不用 DNS 解析直接判断域名是否直连 56 | 57 | ## 直连列表 58 | 59 | 直接连接的域名列表保存在 `~/.meow/direct` (OS X, Linux) 或 `direct.txt` (Windows) 60 | 61 | 匹配域名**按 . 分隔的后两部分**或者**整个域名**,例子: 62 | 63 | - `baidu.com` => `*.baidu.com` 64 | - `com.cn` => `*.com.cn` 65 | - `edu.cn` => `*.edu.cn` 66 | - `music.163.com` => `music.163.com` 67 | 68 | 一般是**确定**要直接连接的网站 69 | 70 | ## 强制使用代理列表 71 | 72 | 强制使用代理连接的域名列表保存在 `~/.meow/proxy` (OS X, Linux) 或 `proxy.txt` (Windows),语法格式与直连列表相同 73 | (注意:匹配的是域名**按 . 分隔的后两部分**或者**整个域名**) 74 | 75 | ## 致谢 76 | 77 | - @cyfdecyf - COW author 78 | - @renzhn - Original MEOW author 79 | - Github - Github Student Pack 80 | - https://www.pandafan.org/pac/index.html - Domain White List 81 | - https://github.com/Leask/Flora_Pac - CN IP Data 82 | - https://github.com/17mon/china_ip_list - CN IP Data 83 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "github.com/cyfdecyf/bufio" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "text/template" 14 | "time" 15 | ) 16 | 17 | const ( 18 | authRealm = "meow proxy" 19 | authRawBodyTmpl = ` 20 | 21 | meow Proxy 22 | 23 |

407 Proxy authentication required

24 |
25 | Generated by meow 26 | 27 | 28 | ` 29 | ) 30 | 31 | type netAddr struct { 32 | ip net.IP 33 | mask net.IPMask 34 | } 35 | 36 | type authUser struct { 37 | // user name is the key to auth.user, no need to store here 38 | passwd string 39 | ha1 string // used in request digest, initialized ondemand 40 | port uint16 // 0 means any port 41 | } 42 | 43 | var auth struct { 44 | required bool 45 | 46 | user map[string]*authUser 47 | 48 | allowedClient []netAddr 49 | 50 | authed *TimeoutSet // cache authenticated users based on ip 51 | 52 | template *template.Template 53 | } 54 | 55 | func (au *authUser) initHA1(user string) { 56 | if au.ha1 == "" { 57 | au.ha1 = md5sum(user + ":" + authRealm + ":" + au.passwd) 58 | } 59 | } 60 | 61 | func parseUserPasswd(userPasswd string) (user string, au *authUser, err error) { 62 | arr := strings.Split(userPasswd, ":") 63 | n := len(arr) 64 | if n == 1 || n > 3 { 65 | err = errors.New("user password: " + userPasswd + 66 | " syntax wrong, should be username:password[:port]") 67 | return 68 | } 69 | user, passwd := arr[0], arr[1] 70 | if user == "" || passwd == "" { 71 | err = errors.New("user password " + userPasswd + 72 | " should not contain empty user name or password") 73 | return "", nil, err 74 | } 75 | var port int 76 | if n == 3 && arr[2] != "" { 77 | port, err = strconv.Atoi(arr[2]) 78 | if err != nil || port <= 0 || port > 0xffff { 79 | err = errors.New("user password: " + userPasswd + " invalid port") 80 | return "", nil, err 81 | } 82 | } 83 | au = &authUser{passwd, "", uint16(port)} 84 | return user, au, nil 85 | } 86 | 87 | func parseAllowedClient(val string) { 88 | if val == "" { 89 | return 90 | } 91 | arr := strings.Split(val, ",") 92 | auth.allowedClient = make([]netAddr, len(arr)) 93 | for i, v := range arr { 94 | s := strings.TrimSpace(v) 95 | ipAndMask := strings.Split(s, "/") 96 | if len(ipAndMask) > 2 { 97 | Fatal("allowedClient syntax error: client should be the form ip/nbitmask") 98 | } 99 | ip := net.ParseIP(ipAndMask[0]) 100 | if ip == nil { 101 | Fatalf("allowedClient syntax error %s: ip address not valid\n", s) 102 | } 103 | var mask net.IPMask 104 | if len(ipAndMask) == 2 { 105 | nbit, err := strconv.Atoi(ipAndMask[1]) 106 | if err != nil { 107 | Fatalf("allowedClient syntax error %s: %v\n", s, err) 108 | } 109 | if nbit > 32 { 110 | Fatal("allowedClient error: mask number should <= 32") 111 | } 112 | mask = NewNbitIPv4Mask(nbit) 113 | } else { 114 | mask = NewNbitIPv4Mask(32) 115 | } 116 | auth.allowedClient[i] = netAddr{ip.Mask(mask), mask} 117 | } 118 | } 119 | 120 | func addUserPasswd(val string) { 121 | if val == "" { 122 | return 123 | } 124 | user, au, err := parseUserPasswd(val) 125 | debug.Println("user:", user, "port:", au.port) 126 | if err != nil { 127 | Fatal(err) 128 | } 129 | if _, ok := auth.user[user]; ok { 130 | Fatal("duplicate user:", user) 131 | } 132 | auth.user[user] = au 133 | } 134 | 135 | func loadUserPasswdFile(file string) { 136 | if file == "" { 137 | return 138 | } 139 | f, err := os.Open(file) 140 | if err != nil { 141 | Fatal("error opening user passwd fle:", err) 142 | } 143 | 144 | r := bufio.NewReader(f) 145 | s := bufio.NewScanner(r) 146 | for s.Scan() { 147 | addUserPasswd(s.Text()) 148 | } 149 | f.Close() 150 | } 151 | 152 | func initAuth() { 153 | if config.UserPasswd != "" || 154 | config.UserPasswdFile != "" || 155 | config.AllowedClient != "" { 156 | auth.required = true 157 | } else { 158 | return 159 | } 160 | 161 | auth.user = make(map[string]*authUser) 162 | 163 | addUserPasswd(config.UserPasswd) 164 | loadUserPasswdFile(config.UserPasswdFile) 165 | parseAllowedClient(config.AllowedClient) 166 | 167 | auth.authed = NewTimeoutSet(time.Duration(config.AuthTimeout) * time.Hour) 168 | 169 | rawTemplate := "HTTP/1.1 407 Proxy Authentication Required\r\n" + 170 | "Proxy-Authenticate: Digest realm=\"" + authRealm + "\", nonce=\"{{.Nonce}}\", qop=\"auth\"\r\n" + 171 | "Content-Type: text/html\r\n" + 172 | "Cache-Control: no-cache\r\n" + 173 | "Content-Length: " + fmt.Sprintf("%d", len(authRawBodyTmpl)) + "\r\n\r\n" + authRawBodyTmpl 174 | var err error 175 | if auth.template, err = template.New("auth").Parse(rawTemplate); err != nil { 176 | Fatal("internal error generating auth template:", err) 177 | } 178 | } 179 | 180 | // Return err = nil if authentication succeed. nonce would be not empty if 181 | // authentication is needed, and should be passed back on subsequent call. 182 | func Authenticate(conn *clientConn, r *Request) (err error) { 183 | clientIP, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) 184 | if auth.authed.has(clientIP) { 185 | debug.Printf("%s has already authed\n", clientIP) 186 | return 187 | } 188 | if authIP(clientIP) { // IP is allowed 189 | return 190 | } 191 | err = authUserPasswd(conn, r) 192 | if err == nil { 193 | auth.authed.add(clientIP) 194 | } 195 | return 196 | } 197 | 198 | // authIP checks whether the client ip address matches one in allowedClient. 199 | // It uses a sequential search. 200 | func authIP(clientIP string) bool { 201 | ip := net.ParseIP(clientIP) 202 | if ip == nil { 203 | panic("authIP should always get IP address") 204 | } 205 | 206 | for _, na := range auth.allowedClient { 207 | if ip.Mask(na.mask).Equal(na.ip) { 208 | debug.Printf("client ip %s allowed\n", clientIP) 209 | return true 210 | } 211 | } 212 | return false 213 | } 214 | 215 | func genNonce() string { 216 | buf := new(bytes.Buffer) 217 | fmt.Fprintf(buf, "%x", time.Now().Unix()) 218 | return buf.String() 219 | } 220 | 221 | func calcRequestDigest(kv map[string]string, ha1, method string) string { 222 | // Refer to rfc2617 section 3.2.2.1 Request-Digest 223 | arr := []string{ 224 | ha1, 225 | kv["nonce"], 226 | kv["nc"], 227 | kv["cnonce"], 228 | "auth", 229 | md5sum(method + ":" + kv["uri"]), 230 | } 231 | return md5sum(strings.Join(arr, ":")) 232 | } 233 | 234 | func checkProxyAuthorization(conn *clientConn, r *Request) error { 235 | if debug { 236 | debug.Printf("cli(%s) authorization: %s\n", conn.RemoteAddr(), r.ProxyAuthorization) 237 | } 238 | 239 | arr := strings.SplitN(r.ProxyAuthorization, " ", 2) 240 | if len(arr) != 2 { 241 | return errors.New("auth: malformed ProxyAuthorization header: " + r.ProxyAuthorization) 242 | } 243 | authMethod := strings.ToLower(strings.TrimSpace(arr[0])) 244 | if authMethod == "digest" { 245 | return authDigest(conn, r, arr[1]) 246 | } else if authMethod == "basic" { 247 | return authBasic(conn, arr[1]) 248 | } 249 | return errors.New("auth: method " + arr[0] + " unsupported, must use digest") 250 | } 251 | 252 | func authPort(conn *clientConn, user string, au *authUser) error { 253 | if au.port == 0 { 254 | return nil 255 | } 256 | _, portStr, _ := net.SplitHostPort(conn.LocalAddr().String()) 257 | port, _ := strconv.Atoi(portStr) 258 | if uint16(port) != au.port { 259 | errl.Printf("cli(%s) auth: user %s port not match\n", conn.RemoteAddr(), user) 260 | return errAuthRequired 261 | } 262 | return nil 263 | } 264 | 265 | func authBasic(conn *clientConn, userPasswd string) error { 266 | b64, err := base64.StdEncoding.DecodeString(userPasswd) 267 | if err != nil { 268 | return errors.New("auth:" + err.Error()) 269 | } 270 | arr := strings.Split(string(b64), ":") 271 | if len(arr) != 2 { 272 | return errors.New("auth: malformed basic auth user:passwd") 273 | } 274 | user := arr[0] 275 | passwd := arr[1] 276 | 277 | au, ok := auth.user[user] 278 | if !ok || au.passwd != passwd { 279 | return errAuthRequired 280 | } 281 | return authPort(conn, user, au) 282 | } 283 | 284 | func authDigest(conn *clientConn, r *Request, keyVal string) error { 285 | authHeader := parseKeyValueList(keyVal) 286 | if len(authHeader) == 0 { 287 | return errors.New("auth: empty authorization list") 288 | } 289 | nonceTime, err := strconv.ParseInt(authHeader["nonce"], 16, 64) 290 | if err != nil { 291 | return fmt.Errorf("auth: nonce %v", err) 292 | } 293 | // If nonce time too early, reject. iOS will create a new connection to do 294 | // authentication. 295 | if time.Now().Sub(time.Unix(nonceTime, 0)) > time.Minute { 296 | return errAuthRequired 297 | } 298 | 299 | user := authHeader["username"] 300 | au, ok := auth.user[user] 301 | if !ok { 302 | errl.Printf("cli(%s) auth: no such user: %s\n", conn.RemoteAddr(), authHeader["username"]) 303 | return errAuthRequired 304 | } 305 | 306 | if err = authPort(conn, user, au); err != nil { 307 | return err 308 | } 309 | if authHeader["qop"] != "auth" { 310 | return errors.New("auth: qop wrong: " + authHeader["qop"]) 311 | } 312 | response, ok := authHeader["response"] 313 | if !ok { 314 | return errors.New("auth: no request-digest response") 315 | } 316 | 317 | au.initHA1(user) 318 | digest := calcRequestDigest(authHeader, au.ha1, r.Method) 319 | if response != digest { 320 | errl.Printf("cli(%s) auth: digest not match, maybe password wrong", conn.RemoteAddr()) 321 | return errAuthRequired 322 | } 323 | return nil 324 | } 325 | 326 | func authUserPasswd(conn *clientConn, r *Request) (err error) { 327 | if r.ProxyAuthorization != "" { 328 | // client has sent authorization header 329 | err = checkProxyAuthorization(conn, r) 330 | if err == nil { 331 | return 332 | } else if err != errAuthRequired { 333 | sendErrorPage(conn, statusBadReq, "Bad authorization request", err.Error()) 334 | return 335 | } 336 | // auth required to through the following 337 | } 338 | 339 | nonce := genNonce() 340 | data := struct { 341 | Nonce string 342 | }{ 343 | nonce, 344 | } 345 | buf := new(bytes.Buffer) 346 | if err := auth.template.Execute(buf, data); err != nil { 347 | return fmt.Errorf("error generating auth response: %v", err) 348 | } 349 | if bool(debug) && verbose { 350 | debug.Printf("authorization response:\n%s", buf.String()) 351 | } 352 | if _, err := conn.Write(buf.Bytes()); err != nil { 353 | return fmt.Errorf("send auth response error: %v", err) 354 | } 355 | return errAuthRequired 356 | } 357 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestParseUserPasswd(t *testing.T) { 9 | testData := []struct { 10 | val string 11 | user string 12 | au *authUser 13 | }{ 14 | {"foo:bar", "foo", &authUser{"bar", "", 0}}, 15 | {"foo:bar:-1", "", nil}, 16 | {"hello:world:", "hello", &authUser{"world", "", 0}}, 17 | {"hello:world:0", "", nil}, 18 | {"hello:world:1024", "hello", &authUser{"world", "", 1024}}, 19 | {"hello:world:65535", "hello", &authUser{"world", "", 65535}}, 20 | } 21 | 22 | for _, td := range testData { 23 | user, au, err := parseUserPasswd(td.val) 24 | if td.au == nil { 25 | if err == nil { 26 | t.Error(td.val, "should return error") 27 | } 28 | continue 29 | } 30 | if td.user != user { 31 | t.Error(td.val, "user should be:", td.user, "got:", user) 32 | } 33 | if td.au.passwd != au.passwd { 34 | t.Error(td.val, "passwd should be:", td.au.passwd, "got:", au.passwd) 35 | } 36 | if td.au.port != au.port { 37 | t.Error(td.val, "port should be:", td.au.port, "got:", au.port) 38 | } 39 | } 40 | } 41 | 42 | func TestCalcDigest(t *testing.T) { 43 | a1 := md5sum("cyf" + ":" + authRealm + ":" + "wlx") 44 | auth := map[string]string{ 45 | "nonce": "50ed159c3b707061418bbb14", 46 | "nc": "00000001", 47 | "cnonce": "6c46874228c087eb", 48 | "uri": "/", 49 | } 50 | const targetDigest = "c51dbb53e998cb7d68794574e60f8ea0" 51 | 52 | digest := calcRequestDigest(auth, a1, "GET") 53 | if digest != targetDigest { 54 | t.Errorf("authentication digest calculation wrong, got: %s, should be: %s\n", digest, targetDigest) 55 | } 56 | } 57 | 58 | func TestParseAllowedClient(t *testing.T) { 59 | parseAllowedClient("") // this should not cause error 60 | 61 | parseAllowedClient("192.168.1.1/16, 192.169.1.2") 62 | 63 | na := &auth.allowedClient[0] 64 | if !na.ip.Equal(net.ParseIP("192.168.0.0")) { 65 | t.Error("ParseAllowedClient 192.168.1.1/16 ip error, got ip:", na.ip) 66 | } 67 | mask := []byte(na.mask) 68 | if mask[0] != 0xff || mask[1] != 0xff || mask[2] != 0 || mask[3] != 0 { 69 | t.Error("ParseAllowedClient 192.168.1.1/16 mask error") 70 | } 71 | 72 | na = &auth.allowedClient[1] 73 | if !na.ip.Equal(net.ParseIP("192.169.1.2")) { 74 | t.Error("ParseAllowedClient 192.169.1.2 ip error") 75 | } 76 | mask = []byte(na.mask) 77 | if mask[0] != 0xff || mask[1] != 0xff || mask[2] != 0xff || mask[3] != 0xff { 78 | t.Error("ParseAllowedClient 192.169.1.2 mask error") 79 | } 80 | } 81 | 82 | func TestAuthIP(t *testing.T) { 83 | parseAllowedClient("192.168.0.0/16, 192.169.2.1, 10.0.0.0/8, 8.8.8.8") 84 | 85 | var testData = []struct { 86 | ip string 87 | allowed bool 88 | }{ 89 | {"10.1.2.3", true}, 90 | {"192.168.1.2", true}, 91 | {"192.169.2.1", true}, 92 | {"192.169.2.2", false}, 93 | {"8.8.8.8", true}, 94 | {"1.2.3.4", false}, 95 | } 96 | 97 | for _, td := range testData { 98 | if authIP(td.ip) != td.allowed { 99 | if td.allowed { 100 | t.Errorf("%s should be allowed\n", td.ip) 101 | } else { 102 | t.Errorf("%s should NOT be allowed\n", td.ip) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /chinaip_gen.go: -------------------------------------------------------------------------------- 1 | // +build generate 2 | // go run chinaip_gen.go 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "encoding/binary" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | // use china ip list database by ipip.net 20 | const ( 21 | chinaIPListFile = "https://github.com/17mon/china_ip_list/raw/master/china_ip_list.txt" 22 | ) 23 | 24 | func main() { 25 | resp, err := http.Get(chinaIPListFile) 26 | if err != nil { 27 | panic(err) 28 | } 29 | if resp.StatusCode != 200 { 30 | panic(fmt.Errorf("Unexpected status %d", resp.StatusCode)) 31 | } 32 | defer resp.Body.Close() 33 | scanner := bufio.NewScanner(resp.Body) 34 | 35 | startList := []string{} 36 | countList := []string{} 37 | 38 | for scanner.Scan() { 39 | line := scanner.Text() 40 | line = strings.TrimSpace(line) 41 | parts := strings.Split(line, "/") 42 | if len(parts) != 2 { 43 | panic(errors.New("Invalid CIDR")) 44 | } 45 | ip := parts[0] 46 | mask := parts[1] 47 | count, err := cidrCalc(mask) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | ipLong, err := ipToUint32(ip) 53 | if err != nil { 54 | panic(err) 55 | } 56 | startList = append(startList, strconv.FormatUint(uint64(ipLong), 10)) 57 | countList = append(countList, strconv.FormatUint(uint64(count), 10)) 58 | } 59 | 60 | file, err := os.OpenFile("chinaip_data.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) 61 | if err != nil { 62 | log.Fatalf("Failed to generate chinaip_data.go: %v", err) 63 | } 64 | defer file.Close() 65 | 66 | fmt.Fprintln(file, "package main") 67 | fmt.Fprint(file, "var CNIPDataStart = []uint32 {\n ") 68 | fmt.Fprint(file, strings.Join(startList, ",\n ")) 69 | fmt.Fprintln(file, ",\n }") 70 | 71 | fmt.Fprint(file, "var CNIPDataNum = []uint{\n ") 72 | fmt.Fprint(file, strings.Join(countList, ",\n ")) 73 | fmt.Fprintln(file, ",\n }") 74 | } 75 | 76 | func cidrCalc(mask string) (uint, error) { 77 | i, err := strconv.Atoi(mask) 78 | if err != nil || i > 32 { 79 | return 0, errors.New("Invalid Mask") 80 | } 81 | p := 32 - i 82 | res := uint(intPow2(p)) 83 | return res, nil 84 | } 85 | 86 | func intPow2(p int) int { 87 | r := 1 88 | for i := 0; i < p; i++ { 89 | r *= 2 90 | } 91 | return r 92 | } 93 | 94 | func ipToUint32(ipstr string) (uint32, error) { 95 | ip := net.ParseIP(ipstr) 96 | if ip == nil { 97 | return 0, errors.New("Invalid IP") 98 | } 99 | ip = ip.To4() 100 | if ip == nil { 101 | return 0, errors.New("Not IPv4") 102 | } 103 | return binary.BigEndian.Uint32(ip), nil 104 | } 105 | -------------------------------------------------------------------------------- /chinaip_init.go: -------------------------------------------------------------------------------- 1 | //go:generate go run chinaip_gen.go 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | "net" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/cyfdecyf/bufio" 14 | ) 15 | 16 | // data range by first byte 17 | var CNIPDataRange [256]struct { 18 | start int 19 | end int 20 | } 21 | 22 | func initCNIPData() { 23 | importCNIPFile() 24 | 25 | n := len(CNIPDataStart) 26 | var curr uint32 27 | var preFirstByte uint32 28 | for i := 0; i < n; i++ { 29 | firstByte := CNIPDataStart[i] >> 24 30 | if curr != firstByte { 31 | curr = firstByte 32 | if preFirstByte != 0 { 33 | CNIPDataRange[preFirstByte].end = i - 1 34 | } 35 | CNIPDataRange[firstByte].start = i 36 | preFirstByte = firstByte 37 | } 38 | } 39 | CNIPDataRange[preFirstByte].end = n - 1 40 | } 41 | 42 | func importCNIPFile() { 43 | if err := isFileExists(config.CNIPFile); err != nil { 44 | return 45 | } 46 | f, err := os.Open(config.CNIPFile) 47 | if err != nil { 48 | errl.Println("Error opening china ip list:", err) 49 | return 50 | } 51 | defer f.Close() 52 | 53 | scanner := bufio.NewScanner(f) 54 | CNIPDataStart = []uint32{} 55 | CNIPDataNum = []uint{} 56 | 57 | for scanner.Scan() { 58 | line := scanner.Text() 59 | line = strings.TrimSpace(line) 60 | parts := strings.Split(line, "/") 61 | if len(parts) != 2 { 62 | panic(errors.New("Invalid CIDR Format")) 63 | } 64 | ip := parts[0] 65 | mask := parts[1] 66 | count, err := cidrCalc(mask) 67 | if err != nil { 68 | panic(err) 69 | } 70 | 71 | ipLong, err := ipToUint32(ip) 72 | if err != nil { 73 | panic(err) 74 | } 75 | CNIPDataStart = append(CNIPDataStart, ipLong) 76 | CNIPDataNum = append(CNIPDataNum, count) 77 | } 78 | debug.Printf("Load china ip list") 79 | } 80 | 81 | func cidrCalc(mask string) (uint, error) { 82 | i, err := strconv.Atoi(mask) 83 | if err != nil || i > 32 { 84 | return 0, errors.New("Invalid Mask") 85 | } 86 | p := 32 - i 87 | res := uint(intPow2(p)) 88 | return res, nil 89 | } 90 | 91 | func intPow2(p int) int { 92 | r := 1 93 | for i := 0; i < p; i++ { 94 | r *= 2 95 | } 96 | return r 97 | } 98 | 99 | func ipToUint32(ipstr string) (uint32, error) { 100 | ip := net.ParseIP(ipstr) 101 | if ip == nil { 102 | return 0, errors.New("Invalid IP") 103 | } 104 | ip = ip.To4() 105 | if ip == nil { 106 | return 0, errors.New("Not IPv4") 107 | } 108 | return binary.BigEndian.Uint32(ip), nil 109 | } 110 | -------------------------------------------------------------------------------- /chinaip_init_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_cidrCalc(t *testing.T) { 8 | type args struct { 9 | mask string 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want uint 15 | wantErr bool 16 | }{ 17 | {"correctness test", args{mask: "18"}, 16384, false}, 18 | {"boundary test", args{mask: "0"}, 4294967296, false}, 19 | {"boundary test", args{mask: "32"}, 1, false}, 20 | {"boundary test", args{mask: "33"}, 0, true}, 21 | {"format test", args{mask: "18\n"}, 0, true}, 22 | {"format test", args{mask: "18 "}, 0, true}, 23 | {"format test", args{mask: "/18"}, 0, true}, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | got, err := cidrCalc(tt.args.mask) 28 | if (err != nil) != tt.wantErr { 29 | t.Errorf("cidrCalc() error = %v, wantErr %v", err, tt.wantErr) 30 | return 31 | } 32 | if got != tt.want { 33 | t.Errorf("cidrCalc() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "path" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/cyfdecyf/bufio" 16 | ) 17 | 18 | const ( 19 | version = "1.5" 20 | defaultListenAddr = "127.0.0.1:4411" 21 | ) 22 | 23 | type LoadBalanceMode byte 24 | 25 | const ( 26 | loadBalanceBackup LoadBalanceMode = iota 27 | loadBalanceHash 28 | loadBalanceLatency 29 | ) 30 | 31 | type Config struct { 32 | RcFile string // config file 33 | LogFile string // path for log file 34 | JudgeByIP bool 35 | LoadBalance LoadBalanceMode // select load balance mode 36 | 37 | SshServer []string 38 | 39 | // authenticate client 40 | UserPasswd string 41 | UserPasswdFile string // file that contains user:passwd:[port] pairs 42 | AllowedClient string 43 | AuthTimeout time.Duration 44 | 45 | // advanced options 46 | DialTimeout time.Duration 47 | ReadTimeout time.Duration 48 | 49 | Core int 50 | 51 | HttpErrorCode int 52 | 53 | dir string // directory containing config file 54 | DirectFile string // direct sites specified by user 55 | ProxyFile string // sites using proxy specified by user 56 | RejectFile string 57 | CNIPFile string 58 | 59 | // not configurable in config file 60 | PrintVer bool 61 | 62 | // not config option 63 | saveReqLine bool // for http and meow parent, should save request line from client 64 | Cert string 65 | Key string 66 | } 67 | 68 | var config Config 69 | var configNeedUpgrade bool // whether should upgrade config file 70 | 71 | func printVersion() { 72 | fmt.Println("MEOW version", version) 73 | } 74 | 75 | func initConfig(rcFile string) { 76 | config.dir = path.Dir(rcFile) 77 | config.DirectFile = path.Join(config.dir, directFname) 78 | config.ProxyFile = path.Join(config.dir, proxyFname) 79 | config.RejectFile = path.Join(config.dir, rejectFname) 80 | config.CNIPFile = path.Join(config.dir, CNIPFname) 81 | 82 | config.JudgeByIP = true 83 | 84 | config.AuthTimeout = 2 * time.Hour 85 | } 86 | 87 | // Whether command line options specifies listen addr 88 | var cmdHasListenAddr bool 89 | 90 | func parseCmdLineConfig() *Config { 91 | var c Config 92 | var listenAddr string 93 | 94 | flag.StringVar(&c.RcFile, "rc", "", "config file, defaults to $HOME/.meow/rc on Unix, ./rc.txt on Windows") 95 | // Specifying listen default value to StringVar would override config file options 96 | flag.StringVar(&listenAddr, "listen", "", "listen address, disables listen in config") 97 | flag.IntVar(&c.Core, "core", 2, "number of cores to use") 98 | flag.StringVar(&c.LogFile, "logFile", "", "write output to file") 99 | flag.BoolVar(&c.PrintVer, "version", false, "print version") 100 | flag.StringVar(&c.Cert, "cert", "", "cert for local https proxy") 101 | flag.StringVar(&c.Key, "key", "", "key for local https proxy") 102 | 103 | flag.Parse() 104 | 105 | if c.RcFile == "" { 106 | c.RcFile = getDefaultRcFile() 107 | } else { 108 | c.RcFile = expandTilde(c.RcFile) 109 | } 110 | if err := isFileExists(c.RcFile); err != nil { 111 | Fatal("fail to get config file:", err) 112 | } 113 | initConfig(c.RcFile) 114 | initDomainList(config.DirectFile, domainTypeDirect) 115 | initDomainList(config.ProxyFile, domainTypeProxy) 116 | initDomainList(config.RejectFile, domainTypeReject) 117 | 118 | if listenAddr != "" { 119 | configParser{}.ParseListen(listenAddr) 120 | cmdHasListenAddr = true // must come after parse 121 | } 122 | return &c 123 | } 124 | 125 | func parseBool(v, msg string) bool { 126 | switch v { 127 | case "true": 128 | return true 129 | case "false": 130 | return false 131 | default: 132 | Fatalf("%s should be true or false\n", msg) 133 | } 134 | return false 135 | } 136 | 137 | func parseInt(val, msg string) (i int) { 138 | var err error 139 | if i, err = strconv.Atoi(val); err != nil { 140 | Fatalf("%s should be an integer\n", msg) 141 | } 142 | return 143 | } 144 | 145 | func parseDuration(val, msg string) (d time.Duration) { 146 | var err error 147 | if d, err = time.ParseDuration(val); err != nil { 148 | Fatalf("%s %v\n", msg, err) 149 | } 150 | return 151 | } 152 | 153 | func checkServerAddr(addr string) error { 154 | _, _, err := net.SplitHostPort(addr) 155 | return err 156 | } 157 | 158 | func isUserPasswdValid(val string) bool { 159 | arr := strings.SplitN(val, ":", 2) 160 | if len(arr) != 2 || arr[0] == "" || arr[1] == "" { 161 | return false 162 | } 163 | return true 164 | } 165 | 166 | // proxyParser provides functions to parse different types of parent proxy 167 | type proxyParser struct{} 168 | 169 | func (p proxyParser) ProxySocks5(val string) { 170 | if err := checkServerAddr(val); err != nil { 171 | Fatal("parent socks server", err) 172 | } 173 | parentProxy.add(newSocksParent(val)) 174 | } 175 | 176 | func (pp proxyParser) ProxyHttp(val string) { 177 | var userPasswd, server string 178 | 179 | idx := strings.LastIndex(val, "@") 180 | if idx == -1 { 181 | server = val 182 | } else { 183 | userPasswd = val[:idx] 184 | server = val[idx+1:] 185 | } 186 | 187 | if err := checkServerAddr(server); err != nil { 188 | Fatal("parent http server", err) 189 | } 190 | 191 | config.saveReqLine = true 192 | 193 | parent := newHttpParent(server) 194 | parent.initAuth(userPasswd) 195 | parentProxy.add(parent) 196 | } 197 | 198 | func (pp proxyParser) ProxyHttps(val string) { 199 | var userPasswd, server string 200 | 201 | idx := strings.LastIndex(val, "@") 202 | if idx == -1 { 203 | server = val 204 | } else { 205 | userPasswd = val[:idx] 206 | server = val[idx+1:] 207 | } 208 | 209 | if err := checkServerAddr(server); err != nil { 210 | Fatal("parent http server", err) 211 | } 212 | 213 | config.saveReqLine = true 214 | 215 | parent := newHttpsParent(server) 216 | parent.initAuth(userPasswd) 217 | parentProxy.add(parent) 218 | } 219 | 220 | // Parse method:passwd@server:port 221 | func parseMethodPasswdServer(val string) (method, passwd, server string, err error) { 222 | // Use the right-most @ symbol to seperate method:passwd and server:port. 223 | idx := strings.LastIndex(val, "@") 224 | if idx == -1 { 225 | err = errors.New("requires both encrypt method and password") 226 | return 227 | } 228 | 229 | methodPasswd := val[:idx] 230 | server = val[idx+1:] 231 | if err = checkServerAddr(server); err != nil { 232 | return 233 | } 234 | 235 | // Password can have : inside, but I don't recommend this. 236 | arr := strings.SplitN(methodPasswd, ":", 2) 237 | if len(arr) != 2 { 238 | err = errors.New("method and password should be separated by :") 239 | return 240 | } 241 | method = arr[0] 242 | passwd = arr[1] 243 | return 244 | } 245 | 246 | // parse shadowsocks proxy 247 | func (pp proxyParser) ProxySs(val string) { 248 | method, passwd, server, err := parseMethodPasswdServer(val) 249 | if err != nil { 250 | Fatal("shadowsocks parent", err) 251 | } 252 | parent := newShadowsocksParent(server) 253 | parent.initCipher(method, passwd) 254 | parentProxy.add(parent) 255 | } 256 | 257 | func (pp proxyParser) ProxyMeow(val string) { 258 | method, passwd, server, err := parseMethodPasswdServer(val) 259 | if err != nil { 260 | Fatal("meow parent", err) 261 | } 262 | 263 | if err := checkServerAddr(server); err != nil { 264 | Fatal("parent meow server", err) 265 | } 266 | 267 | config.saveReqLine = true 268 | parent := newMeowParent(server, method, passwd) 269 | parentProxy.add(parent) 270 | } 271 | 272 | // listenParser provides functions to parse different types of listen addresses 273 | type listenParser struct{} 274 | 275 | func (lp listenParser) ListenHttp(val string, proto string) { 276 | if cmdHasListenAddr { 277 | return 278 | } 279 | 280 | arr := strings.Fields(val) 281 | if len(arr) > 2 { 282 | Fatal("too many fields in listen =", proto, val) 283 | } 284 | 285 | var addr, addrInPAC string 286 | addr = arr[0] 287 | if len(arr) == 2 { 288 | addrInPAC = arr[1] 289 | } 290 | 291 | if err := checkServerAddr(addr); err != nil { 292 | Fatal("listen", proto, "server", err) 293 | } 294 | addListenProxy(newHttpProxy(addr, addrInPAC, proto)) 295 | } 296 | 297 | func (lp listenParser) ListenMeow(val string) { 298 | if cmdHasListenAddr { 299 | return 300 | } 301 | method, passwd, addr, err := parseMethodPasswdServer(val) 302 | if err != nil { 303 | Fatal("listen meow", err) 304 | } 305 | addListenProxy(newMeowProxy(method, passwd, addr)) 306 | } 307 | 308 | // configParser provides functions to parse options in config file. 309 | type configParser struct{} 310 | 311 | func (p configParser) ParseProxy(val string) { 312 | parser := reflect.ValueOf(proxyParser{}) 313 | zeroMethod := reflect.Value{} 314 | 315 | arr := strings.Split(val, "://") 316 | if len(arr) != 2 { 317 | Fatal("proxy has no protocol specified:", val) 318 | } 319 | protocol := arr[0] 320 | 321 | methodName := "Proxy" + strings.ToUpper(protocol[0:1]) + protocol[1:] 322 | method := parser.MethodByName(methodName) 323 | if method == zeroMethod { 324 | Fatalf("no such protocol \"%s\"\n", arr[0]) 325 | } 326 | args := []reflect.Value{reflect.ValueOf(arr[1])} 327 | method.Call(args) 328 | } 329 | 330 | func (p configParser) ParseListen(val string) { 331 | if cmdHasListenAddr { 332 | return 333 | } 334 | 335 | parser := reflect.ValueOf(listenParser{}) 336 | zeroMethod := reflect.Value{} 337 | 338 | var protocol, server string 339 | arr := strings.Split(val, "://") 340 | if len(arr) == 1 { 341 | protocol = "http" 342 | server = val 343 | configNeedUpgrade = true 344 | } else { 345 | protocol = arr[0] 346 | server = arr[1] 347 | } 348 | 349 | methodName := "Listen" + strings.ToUpper(protocol[0:1]) + protocol[1:] 350 | if methodName == "ListenHttps" { 351 | methodName = "ListenHttp" 352 | } 353 | method := parser.MethodByName(methodName) 354 | if method == zeroMethod { 355 | Fatalf("no such listen protocol \"%s\"\n", arr[0]) 356 | } 357 | if methodName == "ListenMeow" { 358 | method.Call([]reflect.Value{reflect.ValueOf(server)}) 359 | } else { 360 | method.Call([]reflect.Value{reflect.ValueOf(server), reflect.ValueOf(protocol)}) 361 | } 362 | } 363 | 364 | func (p configParser) ParseLogFile(val string) { 365 | config.LogFile = expandTilde(val) 366 | } 367 | 368 | func (p configParser) ParseAddrInPAC(val string) { 369 | configNeedUpgrade = true 370 | arr := strings.Split(val, ",") 371 | for i, s := range arr { 372 | if s == "" { 373 | continue 374 | } 375 | s = strings.TrimSpace(s) 376 | host, _, err := net.SplitHostPort(s) 377 | if err != nil { 378 | Fatal("proxy address in PAC", err) 379 | } 380 | if host == "0.0.0.0" { 381 | Fatal("can't use 0.0.0.0 as proxy address in PAC") 382 | } 383 | if hp, ok := listenProxy[i].(*httpProxy); ok { 384 | hp.addrInPAC = s 385 | } else { 386 | Fatal("can't specify address in PAC for non http proxy") 387 | } 388 | } 389 | } 390 | 391 | func (p configParser) ParseSocksParent(val string) { 392 | var pp proxyParser 393 | pp.ProxySocks5(val) 394 | configNeedUpgrade = true 395 | } 396 | 397 | func (p configParser) ParseSshServer(val string) { 398 | arr := strings.Split(val, ":") 399 | if len(arr) == 2 { 400 | val += ":22" 401 | } else if len(arr) == 3 { 402 | if arr[2] == "" { 403 | val += "22" 404 | } 405 | } else { 406 | Fatal("sshServer should be in the form of: user@server:local_socks_port[:server_ssh_port]") 407 | } 408 | // add created socks server 409 | p.ParseSocksParent("127.0.0.1:" + arr[1]) 410 | config.SshServer = append(config.SshServer, val) 411 | } 412 | 413 | var http struct { 414 | parent *httpParent 415 | serverCnt int 416 | passwdCnt int 417 | } 418 | 419 | func (p configParser) ParseHttpParent(val string) { 420 | if err := checkServerAddr(val); err != nil { 421 | Fatal("parent http server", err) 422 | } 423 | config.saveReqLine = true 424 | http.parent = newHttpParent(val) 425 | parentProxy.add(http.parent) 426 | http.serverCnt++ 427 | configNeedUpgrade = true 428 | } 429 | 430 | func (p configParser) ParseHttpUserPasswd(val string) { 431 | if !isUserPasswdValid(val) { 432 | Fatal("httpUserPassword syntax wrong, should be in the form of user:passwd") 433 | } 434 | if http.passwdCnt >= http.serverCnt { 435 | Fatal("must specify httpParent before corresponding httpUserPasswd") 436 | } 437 | http.parent.initAuth(val) 438 | http.passwdCnt++ 439 | } 440 | 441 | func (p configParser) ParseLoadBalance(val string) { 442 | switch val { 443 | case "backup": 444 | config.LoadBalance = loadBalanceBackup 445 | case "hash": 446 | config.LoadBalance = loadBalanceHash 447 | case "latency": 448 | config.LoadBalance = loadBalanceLatency 449 | default: 450 | Fatalf("invalid loadBalance mode: %s\n", val) 451 | } 452 | } 453 | 454 | func (p configParser) ParseDirectFile(val string) { 455 | config.DirectFile = expandTilde(val) 456 | if err := isFileExists(config.DirectFile); err != nil { 457 | Fatal("direct file:", err) 458 | } 459 | } 460 | 461 | func (p configParser) ParseProxyFile(val string) { 462 | config.ProxyFile = expandTilde(val) 463 | if err := isFileExists(config.ProxyFile); err != nil { 464 | Fatal("proxy file:", err) 465 | } 466 | } 467 | 468 | var shadow struct { 469 | parent *shadowsocksParent 470 | passwd string 471 | method string 472 | 473 | serverCnt int 474 | passwdCnt int 475 | methodCnt int 476 | } 477 | 478 | func (p configParser) ParseShadowSocks(val string) { 479 | if shadow.serverCnt-shadow.passwdCnt > 1 { 480 | Fatal("must specify shadowPasswd for every shadowSocks server") 481 | } 482 | // create new shadowsocks parent if both server and password are given 483 | // previously 484 | if shadow.parent != nil && shadow.serverCnt == shadow.passwdCnt { 485 | if shadow.methodCnt < shadow.serverCnt { 486 | shadow.method = "" 487 | shadow.methodCnt = shadow.serverCnt 488 | } 489 | shadow.parent.initCipher(shadow.method, shadow.passwd) 490 | } 491 | if val == "" { // the final call 492 | shadow.parent = nil 493 | return 494 | } 495 | if err := checkServerAddr(val); err != nil { 496 | Fatal("shadowsocks server", err) 497 | } 498 | shadow.parent = newShadowsocksParent(val) 499 | parentProxy.add(shadow.parent) 500 | shadow.serverCnt++ 501 | configNeedUpgrade = true 502 | } 503 | 504 | func (p configParser) ParseShadowPasswd(val string) { 505 | if shadow.passwdCnt >= shadow.serverCnt { 506 | Fatal("must specify shadowSocks before corresponding shadowPasswd") 507 | } 508 | if shadow.passwdCnt+1 != shadow.serverCnt { 509 | Fatal("must specify shadowPasswd for every shadowSocks") 510 | } 511 | shadow.passwd = val 512 | shadow.passwdCnt++ 513 | } 514 | 515 | func (p configParser) ParseShadowMethod(val string) { 516 | if shadow.methodCnt >= shadow.serverCnt { 517 | Fatal("must specify shadowSocks before corresponding shadowMethod") 518 | } 519 | // shadowMethod is optional 520 | shadow.method = val 521 | shadow.methodCnt++ 522 | } 523 | 524 | func checkShadowsocks() { 525 | if shadow.serverCnt != shadow.passwdCnt { 526 | Fatal("number of shadowsocks server and password does not match") 527 | } 528 | // parse the last shadowSocks option again to initialize the last 529 | // shadowsocks server 530 | parser := configParser{} 531 | parser.ParseShadowSocks("") 532 | } 533 | 534 | // Put actual authentication related config parsing in auth.go, so config.go 535 | // doesn't need to know the details of authentication implementation. 536 | 537 | func (p configParser) ParseUserPasswd(val string) { 538 | config.UserPasswd = val 539 | if !isUserPasswdValid(config.UserPasswd) { 540 | Fatal("userPassword syntax wrong, should be in the form of user:passwd") 541 | } 542 | } 543 | 544 | func (p configParser) ParseUserPasswdFile(val string) { 545 | err := isFileExists(val) 546 | if err != nil { 547 | Fatal("userPasswdFile:", err) 548 | } 549 | config.UserPasswdFile = val 550 | } 551 | 552 | func (p configParser) ParseAllowedClient(val string) { 553 | config.AllowedClient = val 554 | } 555 | 556 | func (p configParser) ParseAuthTimeout(val string) { 557 | config.AuthTimeout = parseDuration(val, "authTimeout") 558 | } 559 | 560 | func (p configParser) ParseCore(val string) { 561 | config.Core = parseInt(val, "core") 562 | } 563 | 564 | func (p configParser) ParseHttpErrorCode(val string) { 565 | config.HttpErrorCode = parseInt(val, "httpErrorCode") 566 | } 567 | 568 | func (p configParser) ParseReadTimeout(val string) { 569 | config.ReadTimeout = parseDuration(val, "readTimeout") 570 | } 571 | 572 | func (p configParser) ParseDialTimeout(val string) { 573 | config.DialTimeout = parseDuration(val, "dialTimeout") 574 | } 575 | 576 | func (p configParser) ParseJudgeByIP(val string) { 577 | config.JudgeByIP = parseBool(val, "judgeByIP") 578 | } 579 | 580 | func (p configParser) ParseCert(val string) { 581 | config.Cert = val 582 | } 583 | 584 | func (p configParser) ParseKey(val string) { 585 | config.Key = val 586 | } 587 | 588 | // overrideConfig should contain options from command line to override options 589 | // in config file. 590 | func parseConfig(rc string, override *Config) { 591 | // fmt.Println("rcFile:", path) 592 | f, err := os.Open(expandTilde(rc)) 593 | if err != nil { 594 | Fatal("Error opening config file:", err) 595 | } 596 | 597 | IgnoreUTF8BOM(f) 598 | 599 | scanner := bufio.NewScanner(f) 600 | 601 | parser := reflect.ValueOf(configParser{}) 602 | zeroMethod := reflect.Value{} 603 | var lines []string // store lines for upgrade 604 | 605 | var n int 606 | for scanner.Scan() { 607 | lines = append(lines, scanner.Text()) 608 | 609 | n++ 610 | line := strings.TrimSpace(scanner.Text()) 611 | if line == "" || line[0] == '#' { 612 | continue 613 | } 614 | 615 | v := strings.SplitN(line, "=", 2) 616 | if len(v) != 2 { 617 | Fatal("config syntax error on line", n) 618 | } 619 | key, val := strings.TrimSpace(v[0]), strings.TrimSpace(v[1]) 620 | 621 | methodName := "Parse" + strings.ToUpper(key[0:1]) + key[1:] 622 | method := parser.MethodByName(methodName) 623 | if method == zeroMethod { 624 | Fatalf("no such option \"%s\"\n", key) 625 | } 626 | // for backward compatibility, allow empty string in shadowMethod and logFile 627 | if val == "" && key != "shadowMethod" && key != "logFile" { 628 | Fatalf("empty %s, please comment or remove unused option\n", key) 629 | } 630 | args := []reflect.Value{reflect.ValueOf(val)} 631 | method.Call(args) 632 | } 633 | if scanner.Err() != nil { 634 | Fatalf("Error reading rc file: %v\n", scanner.Err()) 635 | } 636 | f.Close() 637 | 638 | overrideConfig(&config, override) 639 | checkConfig() 640 | 641 | if configNeedUpgrade { 642 | upgradeConfig(rc, lines) 643 | } 644 | } 645 | 646 | func upgradeConfig(rc string, lines []string) { 647 | newrc := rc + ".upgrade" 648 | f, err := os.Create(newrc) 649 | if err != nil { 650 | fmt.Println("can't create upgraded config file") 651 | return 652 | } 653 | 654 | // Upgrade config. 655 | proxyId := 0 656 | listenId := 0 657 | w := bufio.NewWriter(f) 658 | for _, line := range lines { 659 | line := strings.TrimSpace(line) 660 | if line == "" || line[0] == '#' { 661 | w.WriteString(line + newLine) 662 | continue 663 | } 664 | 665 | v := strings.Split(line, "=") 666 | key := strings.TrimSpace(v[0]) 667 | 668 | switch key { 669 | case "listen": 670 | listen := listenProxy[listenId] 671 | listenId++ 672 | w.WriteString(listen.genConfig() + newLine) 673 | // comment out original 674 | w.WriteString("#" + line + newLine) 675 | case "httpParent", "shadowSocks", "socksParent": 676 | backPool, ok := parentProxy.(*backupParentPool) 677 | if !ok { 678 | panic("initial parent pool should be backup pool") 679 | } 680 | parent := backPool.parent[proxyId] 681 | proxyId++ 682 | w.WriteString(parent.genConfig() + newLine) 683 | // comment out original 684 | w.WriteString("#" + line + newLine) 685 | case "httpUserPasswd", "shadowPasswd", "shadowMethod", "addrInPAC": 686 | // just comment out 687 | w.WriteString("#" + line + newLine) 688 | case "proxy": 689 | proxyId++ 690 | w.WriteString(line + newLine) 691 | default: 692 | w.WriteString(line + newLine) 693 | } 694 | } 695 | w.Flush() 696 | f.Close() // Must close file before renaming, otherwise will fail on windows. 697 | 698 | // Rename new and old config file. 699 | if err := os.Rename(rc, rc+"0.8"); err != nil { 700 | fmt.Println("can't backup config file for upgrade:", err) 701 | return 702 | } 703 | if err := os.Rename(newrc, rc); err != nil { 704 | fmt.Println("can't rename upgraded rc to original name:", err) 705 | return 706 | } 707 | } 708 | 709 | func overrideConfig(oldconfig, override *Config) { 710 | newVal := reflect.ValueOf(override).Elem() 711 | oldVal := reflect.ValueOf(oldconfig).Elem() 712 | 713 | // typeOfT := newVal.Type() 714 | for i := 0; i < newVal.NumField(); i++ { 715 | newField := newVal.Field(i) 716 | oldField := oldVal.Field(i) 717 | // log.Printf("%d: %s %s = %v\n", i, 718 | // typeOfT.Field(i).Name, newField.Type(), newField.Interface()) 719 | switch newField.Kind() { 720 | case reflect.String: 721 | s := newField.String() 722 | if s != "" { 723 | oldField.SetString(s) 724 | } 725 | case reflect.Int: 726 | i := newField.Int() 727 | if i != 0 { 728 | oldField.SetInt(i) 729 | } 730 | } 731 | } 732 | } 733 | 734 | // Must call checkConfig before using config. 735 | func checkConfig() { 736 | checkShadowsocks() 737 | // listenAddr must be handled first, as addrInPAC dependends on this. 738 | if listenProxy == nil { 739 | listenProxy = []Proxy{newHttpProxy(defaultListenAddr, "", "http")} 740 | } 741 | } 742 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseListen(t *testing.T) { 8 | parser := configParser{} 9 | parser.ParseListen("http://127.0.0.1:8888") 10 | 11 | hp, ok := listenProxy[0].(*httpProxy) 12 | if !ok { 13 | t.Error("listen http proxy type wrong") 14 | } 15 | if hp.addr != "127.0.0.1:8888" { 16 | t.Error("listen http server address parse error") 17 | } 18 | 19 | parser.ParseListen("http://127.0.0.1:8888 1.2.3.4:5678") 20 | hp, ok = listenProxy[1].(*httpProxy) 21 | if hp.addrInPAC != "1.2.3.4:5678" { 22 | t.Error("listen http addrInPAC parse error") 23 | } 24 | } 25 | 26 | func TestParseProxy(t *testing.T) { 27 | pool, ok := parentProxy.(*backupParentPool) 28 | if !ok { 29 | t.Fatal("parentPool by default should be backup pool") 30 | } 31 | cnt := -1 32 | 33 | var parser configParser 34 | parser.ParseProxy("http://127.0.0.1:8080") 35 | cnt++ 36 | 37 | hp, ok := pool.parent[cnt].ParentProxy.(*httpParent) 38 | if !ok { 39 | t.Fatal("1st http proxy parsed not as httpParent") 40 | } 41 | if hp.server != "127.0.0.1:8080" { 42 | t.Error("1st http proxy server address wrong, got:", hp.server) 43 | } 44 | 45 | parser.ParseProxy("http://user:passwd@127.0.0.2:9090") 46 | cnt++ 47 | hp, ok = pool.parent[cnt].ParentProxy.(*httpParent) 48 | if !ok { 49 | t.Fatal("2nd http proxy parsed not as httpParent") 50 | } 51 | if hp.server != "127.0.0.2:9090" { 52 | t.Error("2nd http proxy server address wrong, got:", hp.server) 53 | } 54 | if hp.authHeader == nil { 55 | t.Error("2nd http proxy server user password not parsed") 56 | } 57 | 58 | parser.ParseProxy("socks5://127.0.0.1:1080") 59 | cnt++ 60 | sp, ok := pool.parent[cnt].ParentProxy.(*socksParent) 61 | if !ok { 62 | t.Fatal("socks proxy parsed not as socksParent") 63 | } 64 | if sp.server != "127.0.0.1:1080" { 65 | t.Error("socks server address wrong, got:", sp.server) 66 | } 67 | 68 | parser.ParseProxy("ss://aes-256-cfb:foobar!@127.0.0.1:1080") 69 | cnt++ 70 | _, ok = pool.parent[cnt].ParentProxy.(*shadowsocksParent) 71 | if !ok { 72 | t.Fatal("shadowsocks proxy parsed not as shadowsocksParent") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "path" 7 | ) 8 | 9 | const ( 10 | rcFname = "rc" 11 | directFname = "direct" 12 | proxyFname = "proxy" 13 | rejectFname = "reject" 14 | CNIPFname = "china_ip_list" 15 | 16 | newLine = "\n" 17 | ) 18 | 19 | func getDefaultRcFile() string { 20 | return path.Join(path.Join(getUserHomeDir(), ".meow", rcFname)) 21 | } 22 | -------------------------------------------------------------------------------- /config_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | const ( 9 | rcFname = "rc.txt" 10 | directFname = "direct.txt" 11 | proxyFname = "proxy.txt" 12 | rejectFname = "reject.txt" 13 | CNIPFname = "china_ip_list.txt" 14 | 15 | newLine = "\r\n" 16 | ) 17 | 18 | func getDefaultRcFile() string { 19 | // On windows, put the configuration file in the same directory of meow executable 20 | // This is not a reliable way to detect binary directory, but it works for double click and run 21 | return path.Join(path.Dir(os.Args[0]), rcFname) 22 | } 23 | -------------------------------------------------------------------------------- /conn_pool.go: -------------------------------------------------------------------------------- 1 | // Share server connections between different clients. 2 | 3 | package main 4 | 5 | import ( 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Maximum number of connections to a server. 11 | const maxServerConnCnt = 5 12 | 13 | // Store each server's connections in separate channels, getting 14 | // connections for different servers can be done in parallel. 15 | type ConnPool struct { 16 | idleConn map[string]chan *serverConn 17 | muxConn chan *serverConn // connections support multiplexing 18 | sync.RWMutex 19 | } 20 | 21 | var connPool = &ConnPool{ 22 | idleConn: map[string]chan *serverConn{}, 23 | muxConn: make(chan *serverConn, maxServerConnCnt*2), 24 | } 25 | 26 | const muxConnHostPort = "@muxConn" 27 | 28 | func init() { 29 | // make sure hostPort here won't match any actual hostPort 30 | go closeStaleServerConn(connPool.muxConn, muxConnHostPort) 31 | } 32 | 33 | func getConnFromChan(ch chan *serverConn) (sv *serverConn) { 34 | for { 35 | select { 36 | case sv = <-ch: 37 | if sv.mayBeClosed() { 38 | sv.Close() 39 | continue 40 | } 41 | return sv 42 | default: 43 | return nil 44 | } 45 | } 46 | } 47 | 48 | func putConnToChan(sv *serverConn, ch chan *serverConn, chname string) { 49 | select { 50 | case ch <- sv: 51 | debug.Printf("connPool channel %s: put conn\n", chname) 52 | return 53 | default: 54 | // Simply close the connection if can't put into channel immediately. 55 | // A better solution would remove old connections from the channel and 56 | // add the new one. But's it's more complicated and this should happen 57 | // rarely. 58 | debug.Printf("connPool channel %s: full", chname) 59 | sv.Close() 60 | } 61 | } 62 | 63 | func (cp *ConnPool) Get(hostPort string, direct bool) (sv *serverConn) { 64 | // Get from site specific connection first. 65 | // Direct connection are all site specific, so must use site specific 66 | // first to avoid using parent proxy for direct sites. 67 | cp.RLock() 68 | ch := cp.idleConn[hostPort] 69 | cp.RUnlock() 70 | 71 | if ch != nil { 72 | sv = getConnFromChan(ch) 73 | } 74 | if sv != nil { 75 | debug.Printf("connPool %s: get conn\n", hostPort) 76 | return sv 77 | } 78 | 79 | // All mulplexing connections are for blocked sites, 80 | // so for direct sites we should stop here. 81 | if direct { 82 | return nil 83 | } 84 | 85 | sv = getConnFromChan(cp.muxConn) 86 | if bool(debug) && sv != nil { 87 | debug.Println("connPool mux: get conn", hostPort) 88 | } 89 | return sv 90 | } 91 | 92 | func (cp *ConnPool) Put(sv *serverConn) { 93 | // Multiplexing connections. 94 | switch sv.Conn.(type) { 95 | case httpConn, meowConn: 96 | putConnToChan(sv, cp.muxConn, "muxConn") 97 | return 98 | } 99 | 100 | // Site specific connections. 101 | cp.RLock() 102 | ch := cp.idleConn[sv.hostPort] 103 | cp.RUnlock() 104 | 105 | if ch == nil { 106 | debug.Printf("connPool %s: new channel\n", sv.hostPort) 107 | ch = make(chan *serverConn, maxServerConnCnt) 108 | ch <- sv 109 | cp.Lock() 110 | cp.idleConn[sv.hostPort] = ch 111 | cp.Unlock() 112 | // start a new goroutine to close stale server connections 113 | go closeStaleServerConn(ch, sv.hostPort) 114 | } else { 115 | putConnToChan(sv, ch, sv.hostPort) 116 | } 117 | } 118 | 119 | type chanInPool struct { 120 | hostPort string 121 | ch chan *serverConn 122 | } 123 | 124 | func (cp *ConnPool) CloseAll() { 125 | debug.Println("connPool: close all server connections") 126 | 127 | // Because closeServerConn may acquire connPool.Lock, we first collect all 128 | // channel, and close server connection for each one. 129 | var connCh []chanInPool 130 | cp.RLock() 131 | for hostPort, ch := range cp.idleConn { 132 | connCh = append(connCh, chanInPool{hostPort, ch}) 133 | } 134 | cp.RUnlock() 135 | 136 | for _, hc := range connCh { 137 | closeServerConn(hc.ch, hc.hostPort, true) 138 | } 139 | 140 | closeServerConn(cp.muxConn, muxConnHostPort, true) 141 | } 142 | 143 | func closeServerConn(ch chan *serverConn, hostPort string, force bool) (done bool) { 144 | // If force is true, close all idle connection even if it maybe open. 145 | lcnt := len(ch) 146 | if lcnt == 0 { 147 | // Execute the loop at least once. 148 | lcnt = 1 149 | } 150 | for i := 0; i < lcnt; i++ { 151 | select { 152 | case sv := <-ch: 153 | if force || sv.mayBeClosed() { 154 | debug.Printf("connPool channel %s: close one conn\n", hostPort) 155 | sv.Close() 156 | } else { 157 | // Put it back and wait. 158 | debug.Printf("connPool channel %s: put back conn\n", hostPort) 159 | ch <- sv 160 | } 161 | default: 162 | if hostPort != muxConnHostPort { 163 | // No more connection in this channel, remove the channel from 164 | // the map. 165 | debug.Printf("connPool channel %s: remove\n", hostPort) 166 | connPool.Lock() 167 | delete(connPool.idleConn, hostPort) 168 | connPool.Unlock() 169 | } 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | 176 | func closeStaleServerConn(ch chan *serverConn, hostPort string) { 177 | // Tricky here. When removing a channel from the map, there maybe 178 | // goroutines doing Put and Get using that channel. 179 | 180 | // For Get, there's no problem because it will return immediately. 181 | // For Put, it's possible that a new connection is added to the 182 | // channel, but the channel is no longer in the map. 183 | // So after removed the channel from the map, we wait for several seconds 184 | // and then close all connections left in it. 185 | 186 | // It's possible that Put add the connection after the final wait, but 187 | // that should not happen in practice, and the worst result is just lost 188 | // some memory and open fd. 189 | for { 190 | time.Sleep(5 * time.Second) 191 | if done := closeServerConn(ch, hostPort, false); done { 192 | break 193 | } 194 | } 195 | // Final wait and then close all left connections. In practice, there 196 | // should be no other goroutines holding reference to the channel. 197 | time.Sleep(2 * time.Second) 198 | for { 199 | select { 200 | case sv := <-ch: 201 | debug.Printf("connPool channel %s: close conn after removed\n", hostPort) 202 | sv.Close() 203 | default: 204 | debug.Printf("connPool channel %s: cleanup done\n", hostPort) 205 | return 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /conn_pool_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestGetFromEmptyPool(t *testing.T) { 9 | // should not block 10 | sv := connPool.Get("foo", true) 11 | if sv != nil { 12 | t.Error("get non nil server conn from empty conn pool") 13 | } 14 | } 15 | 16 | func TestConnPool(t *testing.T) { 17 | closeOn := time.Now().Add(10 * time.Second) 18 | conns := []*serverConn{ 19 | {hostPort: "example.com:80", willCloseOn: closeOn}, 20 | {hostPort: "example.com:80", willCloseOn: closeOn}, 21 | {hostPort: "example.com:80", willCloseOn: closeOn}, 22 | {hostPort: "example.com:443", willCloseOn: closeOn}, 23 | {hostPort: "google.com:443", willCloseOn: closeOn}, 24 | {hostPort: "google.com:443", willCloseOn: closeOn}, 25 | {hostPort: "www.google.com:80", willCloseOn: closeOn}, 26 | } 27 | for _, sv := range conns { 28 | connPool.Put(sv) 29 | } 30 | 31 | testData := []struct { 32 | hostPort string 33 | found bool 34 | }{ 35 | {"example.com", false}, 36 | {"example.com:80", true}, 37 | {"example.com:80", true}, 38 | {"example.com:80", true}, 39 | {"example.com:80", false}, // has 3 such conn 40 | {"www.google.com:80", true}, 41 | } 42 | 43 | for _, td := range testData { 44 | sv := connPool.Get(td.hostPort, true) 45 | if td.found { 46 | if sv == nil { 47 | t.Error("should find conn for", td.hostPort) 48 | } else if sv.hostPort != td.hostPort { 49 | t.Errorf("hostPort should be: %s, got: %s\n", td.hostPort, sv.hostPort) 50 | } 51 | } else if sv != nil { 52 | t.Errorf("should NOT find conn for %s, got conn for: %s\n", 53 | td.hostPort, sv.hostPort) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /directip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ipShouldDirect(ip string) (direct bool) { 8 | if strings.Contains(ip, ":") { 9 | // IPv6 addresses are connected directly 10 | return true 11 | } 12 | direct = false 13 | defer func() { 14 | if r := recover(); r != nil { 15 | errl.Printf("error judging ip should direct: %s", ip) 16 | } 17 | }() 18 | _, isPrivate := hostIsIP(ip) 19 | if isPrivate { 20 | return true 21 | } 22 | ipLong, err := ip2long(ip) 23 | if err != nil { 24 | return false 25 | } 26 | if ipLong == 0 { 27 | return true 28 | } 29 | firstByte := ipLong >> 24 30 | if CNIPDataRange[firstByte].end == 0 { 31 | return false 32 | } 33 | ipIndex := searchRange(CNIPDataRange[firstByte].start, CNIPDataRange[firstByte].end, func(i int) bool { 34 | return CNIPDataStart[i] > ipLong 35 | }) 36 | ipIndex-- 37 | return ipLong <= CNIPDataStart[ipIndex]+(uint32)(CNIPDataNum[ipIndex]) 38 | } 39 | -------------------------------------------------------------------------------- /directip_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestIPShouldDirect(t *testing.T) { 9 | 10 | initCNIPData() 11 | 12 | blockedIPDomains := []string{ 13 | "gist.github.com", 14 | "twitter.com", 15 | } 16 | for _, domain := range blockedIPDomains { 17 | hostIPs, err := net.LookupIP(domain) 18 | 19 | if err != nil { 20 | continue 21 | } 22 | 23 | var ip string 24 | ip = hostIPs[0].String() 25 | 26 | if ipShouldDirect(ip) { 27 | t.Errorf("ip %s should be considered using proxy, domain: %s", ip, domain) 28 | } 29 | } 30 | 31 | directIPDomains := []string{ 32 | "baidu.com", 33 | "www.ahut.edu.cn", 34 | "bt.byr.cn", 35 | } 36 | for _, domain := range directIPDomains { 37 | hostIPs, err := net.LookupIP(domain) 38 | 39 | if err != nil { 40 | continue 41 | } 42 | 43 | var ip string 44 | ip = hostIPs[0].String() 45 | 46 | if !ipShouldDirect(ip) { 47 | t.Errorf("ip %s should be considered using direct, domain: %s", ip, domain) 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /directlist.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/cyfdecyf/bufio" 10 | ) 11 | 12 | type DomainList struct { 13 | Domain map[string]DomainType 14 | sync.RWMutex 15 | } 16 | 17 | type DomainType byte 18 | 19 | const ( 20 | domainTypeUnknown DomainType = iota 21 | domainTypeDirect 22 | domainTypeProxy 23 | domainTypeReject 24 | ) 25 | 26 | func newDomainList() *DomainList { 27 | return &DomainList{ 28 | Domain: map[string]DomainType{}, 29 | } 30 | } 31 | 32 | func (domainList *DomainList) judge(url *URL) (domainType DomainType) { 33 | debug.Printf("judging host: %s", url.Host) 34 | if domainList.Domain[url.Host] == domainTypeReject || domainList.Domain[url.Domain] == domainTypeReject { 35 | debug.Printf("host or domain should reject") 36 | return domainTypeReject 37 | } 38 | if parentProxy.empty() { // no way to retry, so always visit directly 39 | return domainTypeDirect 40 | } 41 | if url.Domain == "" { // simple host or private ip 42 | return domainTypeDirect 43 | } 44 | if domainList.Domain[url.Host] == domainTypeDirect || domainList.Domain[url.Domain] == domainTypeDirect { 45 | debug.Printf("host or domain should direct") 46 | return domainTypeDirect 47 | } 48 | if domainList.Domain[url.Host] == domainTypeProxy || domainList.Domain[url.Domain] == domainTypeProxy { 49 | debug.Printf("host or domain should using proxy") 50 | return domainTypeProxy 51 | } 52 | 53 | if !config.JudgeByIP { 54 | return domainTypeProxy 55 | } 56 | debug.Printf("judging by ip") 57 | var ip string 58 | isIP, isPrivate := hostIsIP(url.Host) 59 | if isIP { 60 | if isPrivate { 61 | domainList.add(url.Host, domainTypeDirect) 62 | return domainTypeDirect 63 | } 64 | ip = url.Host 65 | } else { 66 | hostIPs, err := net.LookupIP(url.Host) 67 | if err != nil { 68 | errl.Printf("error looking up host ip %s, err %s", url.Host, err) 69 | return domainTypeProxy 70 | } 71 | ip = hostIPs[0].String() 72 | } 73 | 74 | if ipShouldDirect(ip) { 75 | domainList.add(url.Host, domainTypeDirect) 76 | debug.Printf("host or domain should direct") 77 | return domainTypeDirect 78 | } else { 79 | domainList.add(url.Host, domainTypeProxy) 80 | debug.Printf("host or domain should using proxy") 81 | return domainTypeProxy 82 | } 83 | } 84 | 85 | func (domainList *DomainList) add(host string, domainType DomainType) { 86 | domainList.Lock() 87 | defer domainList.Unlock() 88 | domainList.Domain[host] = domainType 89 | } 90 | 91 | func (domainList *DomainList) GetDomainList() []string { 92 | lst := make([]string, 0) 93 | for site, domainType := range domainList.Domain { 94 | if domainType == domainTypeDirect { 95 | lst = append(lst, site) 96 | } 97 | } 98 | return lst 99 | } 100 | 101 | var domainList = newDomainList() 102 | 103 | func initDomainList(domainListFile string, domainType DomainType) { 104 | var err error 105 | if err = isFileExists(domainListFile); err != nil { 106 | return 107 | } 108 | f, err := os.Open(domainListFile) 109 | if err != nil { 110 | errl.Println("Error opening domain list:", err) 111 | return 112 | } 113 | defer f.Close() 114 | 115 | domainList.Lock() 116 | defer domainList.Unlock() 117 | scanner := bufio.NewScanner(f) 118 | for scanner.Scan() { 119 | domain := strings.TrimSpace(scanner.Text()) 120 | if domain == "" { 121 | continue 122 | } 123 | debug.Printf("Loaded domain %s as type %v", domain, domainType) 124 | domainList.Domain[domain] = domainType 125 | } 126 | if scanner.Err() != nil { 127 | errl.Printf("Error reading domain list %s: %v\n", domainListFile, scanner.Err()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /directlist_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Testjudge(t *testing.T) { 8 | domainList := newDomainList() 9 | 10 | domainList.Domain["com.cn"] = domainTypeDirect 11 | domainList.Domain["edu.cn"] = domainTypeDirect 12 | domainList.Domain["baidu.com"] = domainTypeDirect 13 | 14 | g, _ := ParseRequestURI("gtemp.com") 15 | if domainList.judge(g) == domainTypeProxy { 16 | t.Error("never visited site should be considered using proxy") 17 | } 18 | 19 | directDomains := []string{ 20 | "baidu.com", 21 | "www.baidu.com", 22 | "www.ahut.edu.cn", 23 | } 24 | for _, domain := range directDomains { 25 | url, _ := ParseRequestURI(domain) 26 | if domainList.judge(url) == domainTypeDirect { 27 | t.Errorf("domain %s in direct list should be considered using direct, host: %s", domain, url.Host) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /doc/implementation.md: -------------------------------------------------------------------------------- 1 | # Design # 2 | 3 | ## Requst and response handling ## 4 | 5 | **Update** using the following design, it is actually difficult to correctly support HTTP pipelining. I've come up with a new design inspired by Naruil which should be much cleaner and easier to support HTTP pipelining. But as all major browsers, except Opera, does not enable HTTP pipelining by default, I don't think it's worth the effort to support HTTP pipelining now. I'll try to support it with the new design if the performance benefits of HTTP pipelining becomes significant in the future. 6 | 7 | The final design is evolved from different previous implementations. The other subsections following this one describe how its evolved. 8 | 9 | meow uses separate goroutines to read client requests and server responses. 10 | 11 | - For each client, meow will create one *request goroutine* to 12 | - accept client request (read from client connection) 13 | - create connection if no one not exist 14 | - send request to the server (write to server connection) 15 | - For each server connection, there will be an associated *response goroutine* 16 | - reading response from the web server (read from server connection) 17 | - send response back to the client (write to client connection) 18 | 19 | One client must have one request goroutine, and may have multiple response goroutine. Response goroutine is created when the server connection is created. 20 | 21 | This makes it possible for meow to support HTTP pipeline. (Not very sure about this.) meow does not pack multiple requests and send in batch, but it can send request before previous request response is received. If the client (browser) and the web server supports HTTP pipeline, then meow will not in effect make them go back to wating response for each request. 22 | 23 | But this design does make meow more complicated. I must be careful to avoid concurrency problems between the request and response goroutine. 24 | 25 | Here's things that worth noting: 26 | 27 | - The web server connection for each host is stored in a map 28 | - The request goroutine creates the connection and put it into this map 29 | - When serving requests, this map will be be used to find already created server connections 30 | - We should avoid writing the map in the response goroutine. So when response goroutine finishes, it should just mark the corresonding connection as closed instead of directly removing it from the map 31 | 32 | - Request and response goroutine may need to notify each other to stop 33 | - When client connection is closed, all response goroutine should stop 34 | - Client connection close can be detected in both request and response goroutine (as both will try to either read or write the connection), to make things simple, I just do notification in the request goroutine 35 | 36 | ## Notification between goroutines 37 | 38 | - Notification sender should not block 39 | - I use a size 1 channel for this as the notification will be sent only once 40 | - Receiver use polling to handle notification 41 | - For blocked calls, should set time out to actively poll notification 42 | 43 | ## Why parse client http request ## 44 | 45 | Of course we need to parse http request to know the address of the web server. 46 | 47 | Besides, HTTP requests sent to proxy servers are a little different from those sent directly to the web servers. So proxy server need to reconstruct http request 48 | 49 | - Normal HTTP 1.1 `GET` request has request URI like '/index.html', but when sending to proxy, it would be something like 'host.com/index.html' 50 | - The `CONNECT` request requires special handling by the proxy (send a 200 back to the client) 51 | 52 | ## Parse http response or not? ## 53 | 54 | The initial implementation serves client request one by one. For each request: 55 | 56 | 1. Parse client HTTP request 57 | 2. Connect to the server and send the request, send the response back to the client 58 | 59 | We need to know whether a response is finished so we can start to serve another request. (This is the oppisite to HTTP pipelining.) That's why we need to parse content-length header and chunked encoding. 60 | 61 | Parsing responses allow the proxy to put server connections back to a pool, thus allows different clients to reuse server connections. 62 | 63 | After supporting `CONNECT`, I realized that I can use a separate goroutine to read HTTP response from the server and pass it directly back to the client. This approach doesn't need to parse response to know when the response ends and then starts to process another request. 64 | 65 | **Update: not parsing HTTP response do have some problems.** Refer to section "But response parsing is necessary". 66 | 67 | This approach has several implications needs to be considered: 68 | 69 | - The proxy doesn't know whether the web server closes the connection by setting the header "Connection: close" 70 | - This should not be a big problem because web server should use persistent connection normally 71 | - And this header is passed directly to the client which would close it's connection to the proxy (even though the proxy didn't close this connection) 72 | - Even if the closed connection header is passed to the client, the client can simply create a new connection to the proxy and the proxy will detect the closed client connection 73 | - The server connection can only serve a single client connection. Because we don't know the boundary of responses, the proxy is unable to identify different responses and sends to different clients 74 | - This means that multiple clients connecting to the same server has to create different server connections 75 | - We have to create multiple connection to the same server to reduce latency any way, but makes it impossible to reuse server connection for different clients 76 | 77 | ### Why choose not parse ### 78 | 79 | I choosed not parsing the response because: 80 | 81 | - Associating client with dedicated server connection is simpler in implementation 82 | - As client could create multiple proxy connections to concurrently issue requests to reduce latency, the proxy can allow only a single connection to different web servers and thus connection pool is not needed 83 | - Not parsing the response reduces overhead 84 | - Need additional goroutine to handle response, so hard to say this definitely has better performance 85 | - If we are going to support HTTP pipelining, we may still need to handle response in separate goroutine 86 | 87 | ### But response parsing is necessary ### 88 | 89 | I've got a bug in handling HTTP response 302 when not parsing the response. 90 | 91 | When trying to visit "youku.com", it gives a "302" response with "Connection: close". The browser doesn't close the connection and still tries to get more content from the server after seeing the response. 92 | 93 | I tried polipo and see it will send back "302" response along with a "Content-Length: 0" to indicate the client that the response has finished. 94 | 95 | To add this kind of response editing capability for my proxy, I have to parse HTTP response. 96 | 97 | So the current solution is to parse the response in the a separate goroutine, which doesn't require lots of code change against the not parsing approach. 98 | 99 | # About supporting auto refresh # 100 | 101 | When blocked sites are detected because of error like connection resets and read time out, we can choose to redo the HTTP request by using parent proxy or just return error page and let the browser refresh. 102 | 103 | I tried to support auto refresh. But as I want support HTTP pipelining, the client request and server response read are in separate goroutine. The response reading goroutine need to send redo request to the client request goroutine and maintain a correct request handling order. The resulting code is very complex and difficult to maintain. Besides, the extra code to support auto refresh may incur performance overhead. 104 | 105 | As blocked sites will be recorded, the refresh is only needed for the first access to a blocked site. Auto refresh is just a minor case optimization. 106 | 107 | So I choose not to support auto refresh as the benefit is small. 108 | 109 | # Error printing policy # 110 | 111 | The goal is **make it easy to find the exact error location**. 112 | 113 | - Error should be printed as early as possible 114 | - If an error happens in a function which will be invoked at multiple places, print the error at the call site 115 | -------------------------------------------------------------------------------- /doc/osx/net.ohrz.meow.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | net.ohrz.meow 7 | ProgramArguments 8 | 9 | MEOWBINARY 10 | 11 | KeepAlive 12 | 13 | NetworkState 14 | 15 | 16 | RunAtLoad 17 | 18 | SoftResourceLimits 19 | 20 | NumberOfFiles 21 | 20480 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /doc/sample-config/check_direct.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | directlist = open('direct').read().splitlines() 5 | 6 | direct_cn = open('direct_cn', 'w') 7 | direct_ok = open('direct_ok', 'w') 8 | direct_fail = open('direct_fail', 'w') 9 | 10 | 11 | for domain in directlist: 12 | if domain.endswith('.cn'): 13 | direct_cn.write(domain + '\n') 14 | print domain + ': cn' 15 | else: 16 | p = subprocess.Popen(['ping', domain], stdout=subprocess.PIPE) 17 | streamdata = p.communicate()[0] 18 | ret = p.returncode 19 | #ret = os.system('ping {}'.format(domain)) 20 | if ret == 0: 21 | direct_ok.write(domain + '\n') 22 | print domain + ': ok' 23 | else: 24 | direct_fail.write(domain + '\n') 25 | print domain + ': fail' 26 | 27 | direct_cn.close() 28 | direct_ok.close() 29 | direct_fail.close() 30 | -------------------------------------------------------------------------------- /doc/sample-config/direct: -------------------------------------------------------------------------------- 1 | com.cn 2 | edu.cn 3 | org.cn 4 | net.cn 5 | gov.cn 6 | weibo.cn 7 | sina.cn 8 | tbcdn.cn 9 | sinajs.cn 10 | amazon.cn 11 | 360.cn 12 | flyme.cn 13 | mtime.cn 14 | 12306.cn 15 | ifanr.cn 16 | kuwo.cn 17 | domob.cn 18 | kuaipan.cn 19 | 3g.cn 20 | sinaimg.cn 21 | tianya.cn 22 | url.cn 23 | blued.cn 24 | 189.cn 25 | 10086.cn 26 | 10010.cn 27 | uc.cn 28 | damai.cn 29 | suning.cn 30 | alimama.cn 31 | liebao.cn 32 | mifile.cn 33 | voicecloud.cn 34 | wps.cn 35 | 8684.cn 36 | uniqlo.cn 37 | bbwc.cn 38 | 3.cn 39 | maxthon.cn 40 | xda.cn 41 | cntv.cn 42 | 6.cn 43 | meizu.cn 44 | 360doc.cn 45 | sto.cn 46 | xiaomi.cn 47 | ccb.cn 48 | macx.cn 49 | d.cn 50 | m1905.cn 51 | t.cn 52 | sh.cn 53 | bong.cn 54 | mafengwo.cn 55 | ucloud.cn 56 | xdf.cn 57 | china.cn 58 | ip.cn 59 | news.cn 60 | linux.cn 61 | dict.cn 62 | windowsazure.cn 63 | dwz.cn 64 | 10010.com 65 | 115.com 66 | 123u.com 67 | 126.com 68 | 126.net 69 | 163.com 70 | 17173.com 71 | 178.com 72 | 17cdn.com 73 | 21cn.com 74 | 2288.org 75 | 3322.org 76 | 360buy.com 77 | 360buyimg.com 78 | 360doc.com 79 | 360safe.com 80 | 36kr.com 81 | 400gb.com 82 | 4399.com 83 | 51.la 84 | 51buy.com 85 | 51cto.com 86 | 51job.com 87 | 51jobcdn.com 88 | 5d6d.com 89 | 5d6d.net 90 | 61.com 91 | 6600.org 92 | 6rooms.com 93 | 7766.org 94 | 7k7k.com 95 | 8800.org 96 | 8866.org 97 | 90g.org 98 | 91.com 99 | 9966.org 100 | acfun.tv 101 | aicdn.com 102 | ali213.net 103 | alibaba.com 104 | alicdn.com 105 | aliexpress.com 106 | aliimg.com 107 | alikunlun.com 108 | alimama.com 109 | alipay.com 110 | alipayobjects.com 111 | alisoft.com 112 | aliyun.com 113 | aliyuncdn.com 114 | aliyuncs.com 115 | anzhi.com 116 | appinn.com 117 | appdownload.itunes.apple.com 118 | apple.com 119 | appsina.com 120 | archlinuxcn.org 121 | atpanel.com 122 | baidu.com 123 | baidupcs.com 124 | baidustatic.com 125 | baifendian.com 126 | baihe.com 127 | baixing.com 128 | bdimg.com 129 | bdstatic.com 130 | bilibili.tv 131 | blogbus.com 132 | blueidea.com 133 | ccb.com 134 | cctv.com 135 | cctvpic.com 136 | cdn20.com 137 | china.com 138 | chinabyte.com 139 | chinacache.com 140 | chinacache.net 141 | chinacaipu.com 142 | chinagba.com 143 | chinahr.com 144 | chinajoy.net 145 | chinamobile.com 146 | chinanetcenter.com 147 | chinanews.com 148 | chinapnr.com 149 | chinaren.com 150 | chinaspeeds.net 151 | chinaunix.net 152 | chinaz.com 153 | chint.com 154 | chiphell.com 155 | chuangxin.com 156 | ci123.com 157 | ciku5.com 158 | citysbs.com 159 | class.coursera.org 160 | cloudcdn.net 161 | cmbchina.com 162 | cmfu.com 163 | cmread.com 164 | cmwb.com 165 | cn.archive.ubuntu.com 166 | cn.bing.com 167 | cn.coremetrics.com 168 | cn.debian.org 169 | cn.msn.com 170 | cn 171 | cnak2.englishtown.com 172 | cnbeta.com 173 | cnbetacdn.com 174 | cnblogs.com 175 | cnepub.com 176 | cnzz.com 177 | comsenz.com 178 | csdn.net 179 | ct10000.com 180 | ctdisk.com 181 | dangdang.com 182 | dbank.com 183 | dedecms.com 184 | diandian.com 185 | dianping.com 186 | discuz.com 187 | discuz.net 188 | dl.google.com 189 | docin.com 190 | donews.com 191 | dospy.com 192 | douban.com 193 | douban.fm 194 | duapp.com 195 | duba.net 196 | duomi.com 197 | duote.com 198 | duowan.com 199 | egou.com 200 | et8.org 201 | etao.com 202 | f3322.org 203 | fantong.com 204 | fenzhi.com 205 | fhldns.com 206 | ganji.com 207 | gaopeng.com 208 | geekpark.net 209 | gfan.com 210 | gtimg.com 211 | hacdn.net 212 | hadns.net 213 | hao123.com 214 | hao123img.com 215 | hc360.com 216 | hdslb.com 217 | hexun.com 218 | hiapk.com 219 | hichina.com 220 | hoopchina.com 221 | huanqiu.com 222 | hudong.com 223 | huochepiao.com 224 | hupu.com 225 | iask.com 226 | iciba.com 227 | idqqimg.com 228 | ifanr.com 229 | ifeng.com 230 | ifengimg.com 231 | ijinshan.com 232 | iqiyi.com 233 | it168.com 234 | itcpn.net 235 | iteye.com 236 | itouzi.com 237 | jandan.net 238 | jd.com 239 | jiashule.com 240 | jiasule.com 241 | jiathis.com 242 | jiayuan.com 243 | jiepang.com 244 | jing.fm 245 | jobbole.com 246 | jstv.com 247 | jumei.com 248 | kaixin001.com 249 | kandian.com 250 | kandian.net 251 | kanimg.com 252 | kankanews.com 253 | kdnet.net 254 | koudai8.com 255 | ku6.com 256 | ku6cdn.com 257 | ku6img.com 258 | kuaidi100.com 259 | kugou.com 260 | lashou.com 261 | letao.com 262 | letv.com 263 | lietou.com 264 | linezing.com 265 | loli.mg 266 | loli.vg 267 | lvping.com 268 | lxdns.com 269 | mangocity.com 270 | mapbar.com 271 | mcbbs.net 272 | mediav.com 273 | meilishuo.com 274 | meituan.com 275 | meituan.net 276 | meizu.com 277 | microsoft.com 278 | miui.com 279 | moe123.com 280 | moegirl.org 281 | mop.com 282 | mtime.com 283 | my-card.in 284 | mydrivers.com 285 | mzstatic.com 286 | netease.com 287 | newsmth.net 288 | ngacn.cc 289 | nuomi.com 290 | okbuy.com 291 | optaim.com 292 | oschina.net 293 | paipai.com 294 | pcbeta.com 295 | pchome.net 296 | pcpop.com 297 | pengyou.com 298 | phoenixlzx.com 299 | phpwind.net 300 | pingan.com 301 | pool.ntp.org 302 | pplive.com 303 | pps.tv 304 | ppstream.com 305 | pptv.com 306 | pubyun.com 307 | qhimg.com 308 | qianlong.com 309 | qidian.com 310 | qingdaonews.com 311 | qiniu.com 312 | qiniudn.com 313 | qiushibaike.com 314 | qiyi.com 315 | qiyipic.com 316 | qq.com 317 | qqmail.com 318 | qstatic.com 319 | qunar.com 320 | qunarzz.com 321 | qvbuy.com 322 | renren.com 323 | renrendai.com 324 | rrfmn.com 325 | rrimg.com 326 | sanguosha.com 327 | sdo.com 328 | sina.com 329 | sinaapp.com 330 | sinaedge.com 331 | sinaimg.com 332 | sinajs.com 333 | skycn.com 334 | smzdm.com 335 | sogou.com 336 | sohu.com 337 | soku.com 338 | solidot.org 339 | soso.com 340 | soufun.com 341 | soufunimg.com 342 | staticfile.org 343 | staticsdo.com 344 | steamcn.com 345 | suning.com 346 | szzfgjj.com 347 | tanx.com 348 | taobao.com 349 | taobaocdn.com 350 | tbcache.com 351 | tdimg.com 352 | tencent.com 353 | tenpay.com 354 | tgbus.com 355 | thawte.com 356 | tiancity.com 357 | tianyaui.com 358 | tiexue.net 359 | tmall.com 360 | tmcdn.net 361 | tom.com 362 | tomonline-inc.com 363 | tuan800.com 364 | tuan800.net 365 | tuanimg.com 366 | tudou.com 367 | tudouui.com 368 | tuniu.com 369 | u148.net 370 | u17.com 371 | ubuntu.com 372 | ucjoy.com 373 | uni-marketers.com 374 | unionpay.com 375 | unionpaysecure.com 376 | upaiyun.com 377 | upyun.com 378 | uusee.com 379 | uuu9.com 380 | vaikan.com 381 | vancl.com 382 | vcimg.com 383 | verycd.com 384 | wandoujia.com 385 | wdjimg.com 386 | weibo.com 387 | weiphone.com 388 | weiyun.com 389 | west263.com 390 | wrating.com 391 | wscdns.com 392 | wumii.com 393 | xdcdn.net 394 | xiachufang.com 395 | xiami.com 396 | xiami.net 397 | xiaomi.com 398 | xiaonei.com 399 | xiazaiba.com 400 | xici.net 401 | xilu.com 402 | xinhuanet.com 403 | xinnet.com 404 | xlpan.com 405 | xnpic.com 406 | xungou.com 407 | xunlei.com 408 | ydstatic.com 409 | yesky.com 410 | yeyou.com 411 | yihaodian.com 412 | yihaodianimg.com 413 | yingjiesheng.com 414 | yintai.com 415 | yinyuetai.com 416 | yiqifa.com 417 | yixun.com 418 | ykimg.com 419 | ynet.com 420 | youdao.com 421 | yougou.com 422 | youku.com 423 | yupoo.com 424 | yy.com 425 | zbjimg.com 426 | zhaopin.com 427 | zhi.hu 428 | zhihu.com 429 | zhimg.com 430 | zhubajie.com 431 | zongheng.com 432 | hi-pda.com 433 | -------------------------------------------------------------------------------- /doc/sample-config/proxy: -------------------------------------------------------------------------------- 1 | google.com 2 | www.google.com.hk 3 | www.google.co.jp 4 | youtube.com 5 | twitter.com 6 | twimg.com 7 | t.co 8 | facebook.com 9 | fbcdn.net 10 | fb.me 11 | -------------------------------------------------------------------------------- /doc/sample-config/rc: -------------------------------------------------------------------------------- 1 | ############################# 2 | # 监听地址,设为0.0.0.0可以监听所有端口,共享给局域网使用 3 | ############################# 4 | listen = http://127.0.0.1:4411 5 | 6 | ############################# 7 | # 指定二级代理 8 | ############################# 9 | # 例子: 10 | # SOCKS5: 11 | # proxy = socks5://127.0.0.1:1080 12 | # HTTP: 13 | # proxy = http://127.0.0.1:8080 14 | # proxy = http://user:password@127.0.0.1:8080 15 | # shadowsocks: 16 | # proxy = ss://encrypt_method:password@1.2.3.4:8388 17 | # 支持的加密方法如下: 18 | # aes-128-cfb, aes-192-cfb, aes-256-cfb, 19 | # bf-cfb, cast5-cfb, des-cfb, rc4-md5, 20 | # chacha20, salsa20, rc4, table 21 | 22 | -------------------------------------------------------------------------------- /doc/sample-config/rc-full: -------------------------------------------------------------------------------- 1 | # 配置文件中 # 开头的行为注释 2 | # 3 | # 代理服务器监听地址,重复多次来指定多个监听地址,语法: 4 | # 5 | # listen = protocol://[optional@]server_address:server_port 6 | # 7 | # 支持的 protocol 如下: 8 | # 9 | # HTTP (提供 http 代理): 10 | # listen = http://127.0.0.1:4411 11 | # 12 | # 上面的例子中,MEOW 生成的 PAC url 为 http://127.0.0.1:4411/pac 13 | # 14 | # HTTPS (提供 https 代理): 15 | # listen = https://example.com:443 16 | # cert = /path/to/cert.pem 17 | # key = /path/to/key.pem 18 | # 19 | # 上面的例子中,MEOW 生成的 PAC url 为 https://example.com:443/pac 20 | # 21 | # MEOW (需两个 MEOW 服务器配合使用): 22 | # listen = meow://encrypt_method:password@1.2.3.4:5678 23 | # 24 | # 若 1.2.3.4:5678 在国外,位于国内的 MEOW 配置其为二级代理后,两个 MEOW 之间可以 25 | # 通过加密连接传输 http 代理流量。采用与 shadowsocks 相同的加密方式。 26 | # 27 | # 其他说明: 28 | # - 若 server_address 为 0.0.0.0,监听本机所有 IP 地址 29 | # - 可以用如下语法指定 PAC 中返回的代理服务器地址(当使用端口映射将 http 代理提供给外网时使用) 30 | # listen = http://127.0.0.1:4411 1.2.3.4:5678 31 | # 32 | listen = http://127.0.0.1:4411 33 | 34 | ############################# 35 | # 通过IP判断是否直连,默认开启 36 | ############################# 37 | #judgeByIP = true 38 | 39 | # 日志文件路径,如不指定则输出到 stdout 40 | #logFile = 41 | 42 | # 指定多个二级代理时使用的负载均衡策略,可选策略如下 43 | # 44 | # backup: 默认策略,优先使用第一个指定的二级代理,其他仅作备份使用 45 | # hash: 根据请求的 host name,优先使用 hash 到的某一个二级代理 46 | # latency: 优先选择连接延迟最低的二级代理 47 | # 48 | # 一个二级代理连接失败后会依次尝试其他二级代理 49 | # 失败的二级代理会以一定的概率再次尝试使用,因此恢复后会重新启用 50 | #loadBalance = backup 51 | 52 | ############################# 53 | # 指定二级代理 54 | ############################# 55 | 56 | # 二级代理统一使用下列语法指定: 57 | # 58 | # proxy = protocol://[authinfo@]server:port 59 | # 60 | # 重复使用 proxy 多次指定多个二级代理,backup 策略将按照二级代理出现的顺序来使用 61 | # 62 | # 目前支持的二级代理及配置举例: 63 | # 64 | # SOCKS5: 65 | # proxy = socks5://127.0.0.1:1080 66 | # 67 | # HTTP: 68 | # proxy = http://127.0.0.1:8080 69 | # proxy = http://user:password@127.0.0.1:8080 70 | # 71 | # 用户认证信息为可选项 72 | # 73 | # HTTPS: 74 | # proxy = https://example.com:8080 75 | # proxy = https://user:password@example.com:8080 76 | # 77 | # 用户认证信息为可选项 78 | # 79 | # Shadowsocks: 80 | # proxy = ss://encrypt_method:password@1.2.3.4:8388 81 | # 82 | # authinfo 中指定加密方法和密码,所有支持的加密方法如下: 83 | # aes-128-cfb, aes-192-cfb, aes-256-cfb, 84 | # bf-cfb, cast5-cfb, des-cfb, rc4-md5, 85 | # chacha20, salsa20, rc4, table 86 | # 87 | # MEOW: 88 | # proxy = meow://method:passwd@1.2.3.4:4321 89 | # 90 | # authinfo 与 shadowsocks 相同 91 | 92 | 93 | ############################# 94 | # 执行 ssh 命令创建 SOCKS5 代理 95 | ############################# 96 | 97 | # 下面的选项可以让 MEOW 执行 ssh 命令创建本地 SOCKS5 代理,并在 ssh 断开后重连 98 | # MEOW 会自动使用通过 ssh 命令创建的代理,无需再通过 proxy 选项指定 99 | # 可重复指定多个 100 | # 101 | # 注意这一功能需要系统上已有 ssh 命令,且必须使用 ssh public key authentication 102 | # 103 | # 若指定该选项,MEOW 将执行以下命令: 104 | # ssh -n -N -D -p 105 | # server_ssh_port 端口不指定则默认为 22 106 | # 如果要指定其他 ssh 选项,请修改 ~/.ssh/config 107 | #sshServer = user@server:local_socks_port[:server_ssh_port] 108 | 109 | ############################# 110 | # 认证 111 | ############################# 112 | 113 | # 指定允许的 IP 或者网段。网段仅支持 IPv4,可以指定 IPv6 地址,用逗号分隔多个项 114 | # 使用此选项时别忘了添加 127.0.0.1,否则本机访问也需要认证 115 | #allowedClient = 127.0.0.1, 192.168.1.0/24, 10.0.0.0/8 116 | 117 | # 要求客户端通过用户名密码认证 118 | # MEOW 总是先验证 IP 是否在 allowedClient 中,若不在其中再通过用户名密码认证 119 | #userPasswd = username:password 120 | 121 | # 如需指定多个用户名密码,可在下面选项指定的文件中列出,文件中每行内容如下 122 | # username:password[:port] 123 | # port 为可选项,若指定,则该用户只能从指定端口连接 MEOW 124 | # 注意:如有重复用户,MEOW 会报错退出 125 | #userPasswdFile = /path/to/file 126 | 127 | # 认证失效时间 128 | # 语法:2h3m4s 表示 2 小时 3 分钟 4 秒 129 | #authTimeout = 2h 130 | 131 | ############################# 132 | # 高级选项 133 | ############################# 134 | 135 | # 将指定的 HTTP error code 认为是被干扰,使用二级代理重试,默认为空 136 | #httpErrorCode = 137 | 138 | # 最多允许使用多少个 CPU 核 139 | #core = 2 140 | 141 | # 修改 direct/proxy 文件路径,如不指定,默认在配置文件所在目录下 142 | #directFile = /direct 143 | #proxyFile = /proxy 144 | -------------------------------------------------------------------------------- /doc/sample-config/reject: -------------------------------------------------------------------------------- 1 | acs86.com 2 | adcome.cn 3 | adinfuse.com 4 | admaster.com.cn 5 | admob.com 6 | adsage.cn 7 | adsage.com 8 | adsmogo.org 9 | ads.mobclix.com 10 | adview.cn 11 | adwhirl.com 12 | adwo.com 13 | appads.com 14 | domob.cn 15 | domob.org 16 | doubleclick.net 17 | duomeng.cn 18 | duomeng.net 19 | duomeng.org 20 | googeadsserving.cn 21 | guomob.com 22 | immob.cn 23 | inmobi.com 24 | mobads.baidu.com 25 | mobads-logs.baidu.com 26 | smartadserver.com 27 | tapjoyads.com 28 | umeng.co 29 | umeng.com 30 | umtrack.com 31 | uyunad.com 32 | youmi.net 33 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "text/template" 8 | "time" 9 | ) 10 | 11 | // Do not end with "\r\n" so we can add more header later 12 | var headRawTmpl = "HTTP/1.1 {{.CodeReason}}\r\n" + 13 | "Connection: keep-alive\r\n" + 14 | "Cache-Control: no-cache\r\n" + 15 | "Pragma: no-cache\r\n" + 16 | "Content-Type: text/html\r\n" + 17 | "Content-Length: {{.Length}}\r\n" 18 | 19 | var errPageTmpl, headTmpl *template.Template 20 | 21 | func init() { 22 | hostName, err := os.Hostname() 23 | if err != nil { 24 | hostName = "unknown host" 25 | } 26 | 27 | errPageRawTmpl := ` 28 | 29 | MEOW Proxy 30 | 31 |

{{.H1}}

32 | {{.Msg}} 33 |
34 | Generated by MEOW ` + version + `
35 | Host ` + hostName + `
36 | {{.T}} 37 | 38 | 39 | ` 40 | if headTmpl, err = template.New("errorHead").Parse(headRawTmpl); err != nil { 41 | Fatal("Internal error on generating error head template") 42 | } 43 | if errPageTmpl, err = template.New("errorPage").Parse(errPageRawTmpl); err != nil { 44 | Fatalf("Internal error on generating error page template") 45 | } 46 | } 47 | 48 | func genErrorPage(h1, msg string) (string, error) { 49 | var err error 50 | data := struct { 51 | H1 string 52 | Msg string 53 | T string 54 | }{ 55 | h1, 56 | msg, 57 | time.Now().Format(time.ANSIC), 58 | } 59 | 60 | buf := new(bytes.Buffer) 61 | err = errPageTmpl.Execute(buf, data) 62 | return buf.String(), err 63 | } 64 | 65 | func sendPageGeneric(w io.Writer, codeReason, h1, msg string) { 66 | page, err := genErrorPage(h1, msg) 67 | if err != nil { 68 | errl.Println("Error generating error page:", err) 69 | return 70 | } 71 | 72 | data := struct { 73 | CodeReason string 74 | Length int 75 | }{ 76 | codeReason, 77 | len(page), 78 | } 79 | buf := new(bytes.Buffer) 80 | if err := headTmpl.Execute(buf, data); err != nil { 81 | errl.Println("Error generating error page header:", err) 82 | return 83 | } 84 | 85 | buf.WriteString("\r\n") 86 | buf.WriteString(page) 87 | w.Write(buf.Bytes()) 88 | } 89 | 90 | func sendErrorPage(w io.Writer, codeReason, h1, msg string) { 91 | sendPageGeneric(w, codeReason, "[Error] "+h1, msg) 92 | } 93 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/cyfdecyf/bufio" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const CRLF = "\r\n" 15 | 16 | const ( 17 | statusCodeContinue = 100 18 | ) 19 | 20 | const ( 21 | statusBadReq = "400 Bad Request" 22 | statusForbidden = "403 Forbidden" 23 | statusExpectFailed = "417 Expectation Failed" 24 | statusRequestTimeout = "408 Request Timeout" 25 | ) 26 | 27 | var CustomHttpErr = errors.New("CustomHttpErr") 28 | 29 | type Header struct { 30 | ContLen int64 31 | KeepAlive time.Duration 32 | ProxyAuthorization string 33 | Chunking bool 34 | Trailer bool 35 | ConnectionKeepAlive bool 36 | ExpectContinue bool 37 | Host string 38 | } 39 | 40 | type rqState byte 41 | 42 | const ( 43 | rsCreated rqState = iota 44 | rsSent // request has been sent to server 45 | rsRecvBody // response header received, receiving response body 46 | rsDone 47 | ) 48 | 49 | type Request struct { 50 | Method string 51 | URL *URL 52 | raw *bytes.Buffer // stores the raw content of request header 53 | rawByte []byte // underlying buffer for raw 54 | 55 | // request line from client starts at 0, meow generates request line that 56 | // can be sent directly to web server 57 | reqLnStart int // start of generated request line in raw 58 | headStart int // start of header in raw 59 | bodyStart int // start of body in raw 60 | 61 | Header 62 | isConnect bool 63 | partial bool // whether contains only partial request data 64 | state rqState 65 | } 66 | 67 | // Assume keep-alive request by default. 68 | var zeroRequest = Request{Header: Header{ConnectionKeepAlive: true}} 69 | 70 | func (r *Request) reset() { 71 | b := r.rawByte 72 | raw := r.raw 73 | *r = zeroRequest // reset to zero value 74 | 75 | if raw != nil { 76 | raw.Reset() 77 | r.rawByte = b 78 | r.raw = raw 79 | } else { 80 | r.rawByte = httpBuf.Get() 81 | r.raw = bytes.NewBuffer(r.rawByte[:0]) // must use 0 length slice 82 | } 83 | } 84 | 85 | func (r *Request) String() (s string) { 86 | return fmt.Sprintf("%s %s%s", r.Method, r.URL.HostPort, r.URL.Path) 87 | } 88 | 89 | func (r *Request) Verbose() []byte { 90 | var rqbyte []byte 91 | if r.isConnect { 92 | rqbyte = r.rawBeforeBody() 93 | } else { 94 | // This includes client request line if has http parent proxy 95 | rqbyte = r.raw.Bytes() 96 | } 97 | return rqbyte 98 | } 99 | 100 | // Message body in request is signaled by the inclusion of a Content-Length 101 | // or Transfer-Encoding header. 102 | // Refer to http://stackoverflow.com/a/299696/306935 103 | func (r *Request) hasBody() bool { 104 | return r.Chunking || r.ContLen > 0 105 | } 106 | 107 | func (r *Request) responseNotSent() bool { 108 | return r.state <= rsSent 109 | } 110 | 111 | func (r *Request) hasSent() bool { 112 | return r.state >= rsSent 113 | } 114 | 115 | func (r *Request) releaseBuf() { 116 | if r.raw != nil { 117 | httpBuf.Put(r.rawByte) 118 | r.rawByte = nil 119 | r.raw = nil 120 | } 121 | } 122 | 123 | // rawRequest returns the raw request that can be sent directly to HTTP/1.1 server. 124 | func (r *Request) rawRequest() []byte { 125 | return r.raw.Bytes()[r.reqLnStart:] 126 | } 127 | 128 | func (r *Request) rawBeforeBody() []byte { 129 | return r.raw.Bytes()[:r.bodyStart] 130 | } 131 | 132 | func (r *Request) rawHeaderBody() []byte { 133 | return r.raw.Bytes()[r.headStart:] 134 | } 135 | 136 | func (r *Request) rawBody() []byte { 137 | return r.raw.Bytes()[r.bodyStart:] 138 | } 139 | 140 | func (r *Request) proxyRequestLine() []byte { 141 | return r.raw.Bytes()[0:r.reqLnStart] 142 | } 143 | 144 | func (r *Request) genRequestLine() { 145 | // Generate normal HTTP request line 146 | r.raw.WriteString(r.Method + " ") 147 | if len(r.URL.Path) == 0 { 148 | r.raw.WriteString("/") 149 | } else { 150 | r.raw.WriteString(r.URL.Path) 151 | } 152 | r.raw.WriteString(" HTTP/1.1\r\n") 153 | } 154 | 155 | type Response struct { 156 | Status int 157 | Reason []byte 158 | 159 | Header 160 | 161 | raw *bytes.Buffer 162 | rawByte []byte 163 | } 164 | 165 | var zeroResponse = Response{Header: Header{ConnectionKeepAlive: true}} 166 | 167 | func (rp *Response) reset() { 168 | b := rp.rawByte 169 | raw := rp.raw 170 | *rp = zeroResponse 171 | 172 | if raw != nil { 173 | raw.Reset() 174 | rp.rawByte = b 175 | rp.raw = raw 176 | } else { 177 | rp.rawByte = httpBuf.Get() 178 | rp.raw = bytes.NewBuffer(rp.rawByte[:0]) 179 | } 180 | } 181 | 182 | func (rp *Response) releaseBuf() { 183 | if rp.raw != nil { 184 | httpBuf.Put(rp.rawByte) 185 | rp.rawByte = nil 186 | rp.raw = nil 187 | } 188 | } 189 | 190 | func (rp *Response) rawResponse() []byte { 191 | return rp.raw.Bytes() 192 | } 193 | 194 | func (rp *Response) genStatusLine() { 195 | rp.raw.Write([]byte("HTTP/1.1 ")) 196 | rp.raw.WriteString(strconv.Itoa(rp.Status)) 197 | if len(rp.Reason) != 0 { 198 | rp.raw.WriteByte(' ') 199 | rp.raw.Write(rp.Reason) 200 | } 201 | rp.raw.Write([]byte(CRLF)) 202 | return 203 | } 204 | 205 | func (rp *Response) String() string { 206 | return fmt.Sprintf("%d %s", rp.Status, rp.Reason) 207 | } 208 | 209 | func (rp *Response) Verbose() []byte { 210 | return rp.raw.Bytes() 211 | } 212 | 213 | type URL struct { 214 | HostPort string // must contain port 215 | Host string // no port 216 | Port string 217 | Domain string 218 | Path string 219 | } 220 | 221 | func (url *URL) String() string { 222 | return url.HostPort + url.Path 223 | } 224 | 225 | // Set all fields according to hostPort except Path. 226 | func (url *URL) ParseHostPort(hostPort string) { 227 | if hostPort == "" { 228 | return 229 | } 230 | host, port, err := net.SplitHostPort(hostPort) 231 | if err != nil { 232 | // Add default 80 and split again. If there's still error this time, 233 | // it's not because lack of port number. 234 | host = hostPort 235 | port = "80" 236 | hostPort = net.JoinHostPort(hostPort, port) 237 | } 238 | 239 | url.Host = host 240 | url.Port = port 241 | url.HostPort = hostPort 242 | url.Domain = host2Domain(host) 243 | } 244 | 245 | // net.ParseRequestURI will unescape encoded path, but the proxy doesn't need 246 | // that. Assumes the input rawurl is valid. Even if rawurl is not valid, net.Dial 247 | // will check the correctness of the host. 248 | 249 | func ParseRequestURI(rawurl string) (*URL, error) { 250 | return ParseRequestURIBytes([]byte(rawurl)) 251 | } 252 | 253 | func ParseRequestURIBytes(rawurl []byte) (*URL, error) { 254 | if rawurl[0] == '/' { 255 | return &URL{Path: string(rawurl)}, nil 256 | } 257 | 258 | var rest, scheme []byte 259 | id := bytes.Index(rawurl, []byte("://")) 260 | if id == -1 { 261 | rest = rawurl 262 | scheme = []byte("http") // default to http 263 | } else { 264 | scheme = rawurl[:id] 265 | ASCIIToLowerInplace(scheme) // it's ok to lower case scheme 266 | if !bytes.Equal(scheme, []byte("http")) && !bytes.Equal(scheme, []byte("https")) { 267 | errl.Printf("%s protocol not supported\n", scheme) 268 | return nil, errors.New("protocol not supported") 269 | } 270 | rest = rawurl[id+3:] 271 | } 272 | 273 | var hostport, host, port, path string 274 | id = bytes.IndexByte(rest, '/') 275 | if id == -1 { 276 | hostport = string(rest) 277 | } else { 278 | hostport = string(rest[:id]) 279 | path = string(rest[id:]) 280 | } 281 | 282 | // Must add port in host so it can be used as key to find the correct 283 | // server connection. 284 | // e.g. google.com:80 and google.com:443 should use different connections. 285 | host, port, err := net.SplitHostPort(hostport) 286 | if err != nil { // missing port 287 | host = hostport 288 | if len(scheme) == 4 { 289 | hostport = net.JoinHostPort(host, "80") 290 | port = "80" 291 | } else { 292 | hostport = net.JoinHostPort(host, "443") 293 | port = "443" 294 | } 295 | } 296 | 297 | // Fixed wechat image url bug, url like http://[::ffff:183.192.196.102]/mmsns/lVxxxxxx 298 | host = strings.TrimSuffix(strings.TrimPrefix(host, "[::ffff:"), "]") 299 | hostport = net.JoinHostPort(host, port) 300 | return &URL{hostport, host, port, host2Domain(host), path}, nil 301 | } 302 | 303 | // headers of interest to a proxy 304 | // Define them as constant and use editor's completion to avoid typos. 305 | // Note RFC2616 only says about "Connection", no "Proxy-Connection", but 306 | // Firefox and Safari send this header along with "Connection" header. 307 | // See more at http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/web-proxy-connection-header.html 308 | const ( 309 | headerConnection = "connection" 310 | headerContentLength = "content-length" 311 | headerExpect = "expect" 312 | headerHost = "host" 313 | headerKeepAlive = "keep-alive" 314 | headerProxyAuthenticate = "proxy-authenticate" 315 | headerProxyAuthorization = "proxy-authorization" 316 | headerProxyConnection = "proxy-connection" 317 | headerReferer = "referer" 318 | headerTE = "te" 319 | headerTrailer = "trailer" 320 | headerTransferEncoding = "transfer-encoding" 321 | headerUpgrade = "upgrade" 322 | 323 | fullHeaderConnectionKeepAlive = "Connection: keep-alive\r\n" 324 | fullHeaderConnectionClose = "Connection: close\r\n" 325 | fullHeaderTransferEncoding = "Transfer-Encoding: chunked\r\n" 326 | ) 327 | 328 | // Using Go's method expression 329 | var headerParser = map[string]HeaderParserFunc{ 330 | headerConnection: (*Header).parseConnection, 331 | headerContentLength: (*Header).parseContentLength, 332 | headerExpect: (*Header).parseExpect, 333 | headerHost: (*Header).parseHost, 334 | headerKeepAlive: (*Header).parseKeepAlive, 335 | headerProxyAuthorization: (*Header).parseProxyAuthorization, 336 | headerProxyConnection: (*Header).parseConnection, 337 | headerTransferEncoding: (*Header).parseTransferEncoding, 338 | headerTrailer: (*Header).parseTrailer, 339 | } 340 | 341 | var hopByHopHeader = map[string]bool{ 342 | headerConnection: true, 343 | headerKeepAlive: true, 344 | headerProxyAuthorization: true, 345 | headerProxyConnection: true, 346 | headerTE: true, 347 | headerTrailer: true, 348 | headerTransferEncoding: true, 349 | headerUpgrade: true, 350 | } 351 | 352 | // Note: Value bytes passed to header parse function are in the buffer 353 | // associated with bufio and will be modified. It will also be stored in the 354 | // raw request buffer, so becareful when modifying the value bytes. (Change 355 | // case only when the spec says it is case insensitive.) 356 | // 357 | // If Header needs to hold raw value, make a copy. For example, 358 | // parseProxyAuthorization does this. 359 | 360 | type HeaderParserFunc func(*Header, []byte) error 361 | 362 | // Used by both "Connection" and "Proxy-Connection" header. meow always adds 363 | // connection header at the end of a request/response (in parseRequest and 364 | // parseResponse), no matter whether the original one has this header or not. 365 | // This will change the order of headers, but should be OK as RFC2616 4.2 says 366 | // header order is not significant. (Though general-header first is "good- 367 | // practice".) 368 | func (h *Header) parseConnection(s []byte) error { 369 | ASCIIToLowerInplace(s) 370 | h.ConnectionKeepAlive = !bytes.Contains(s, []byte("close")) 371 | return nil 372 | } 373 | 374 | func (h *Header) parseContentLength(s []byte) (err error) { 375 | h.ContLen, err = ParseIntFromBytes(s, 10) 376 | return err 377 | } 378 | 379 | func (h *Header) parseHost(s []byte) (err error) { 380 | if h.Host == "" { 381 | h.Host = string(s) 382 | } 383 | return 384 | } 385 | 386 | func (h *Header) parseKeepAlive(s []byte) (err error) { 387 | ASCIIToLowerInplace(s) 388 | id := bytes.Index(s, []byte("timeout=")) 389 | if id != -1 { 390 | id += len("timeout=") 391 | end := id 392 | for ; end < len(s) && IsDigit(s[end]); end++ { 393 | } 394 | delta, err := ParseIntFromBytes(s[id:end], 10) 395 | if err != nil { 396 | return err // possible empty bytes 397 | } 398 | h.KeepAlive = time.Second * time.Duration(delta) 399 | } 400 | return nil 401 | } 402 | 403 | func (h *Header) parseProxyAuthorization(s []byte) error { 404 | h.ProxyAuthorization = string(s) 405 | return nil 406 | } 407 | 408 | func (h *Header) parseTransferEncoding(s []byte) error { 409 | ASCIIToLowerInplace(s) 410 | // For transfer-encoding: identify, it's the same as specifying neither 411 | // content-length nor transfer-encoding. 412 | h.Chunking = bytes.Contains(s, []byte("chunked")) 413 | if !h.Chunking && !bytes.Contains(s, []byte("identity")) { 414 | return fmt.Errorf("invalid transfer encoding: %s", s) 415 | } 416 | return nil 417 | } 418 | 419 | // RFC 2616 3.6.1 states when trailers are allowed: 420 | // 421 | // a) request includes TE header 422 | // b) server is the original server 423 | // 424 | // Even though meow removes TE header, the original server can still respond 425 | // with Trailer header. 426 | // As Trailer is general header, it's possible to appear in request. But is 427 | // there any client does this? 428 | func (h *Header) parseTrailer(s []byte) error { 429 | // use errl to test if this header is common to see 430 | errl.Printf("got Trailer header: %s\n", s) 431 | if len(s) != 0 { 432 | h.Trailer = true 433 | } 434 | return nil 435 | } 436 | 437 | // For now, meow does not fully support 100-continue. It will return "417 438 | // expectation failed" if a request contains expect header. This is one of the 439 | // strategies supported by polipo, which is easiest to implement in meow. 440 | // TODO If we see lots of expect 100-continue usage, provide full support. 441 | 442 | func (h *Header) parseExpect(s []byte) error { 443 | ASCIIToLowerInplace(s) 444 | errl.Printf("Expect header: %s\n", s) // put here to see if expect header is widely used 445 | h.ExpectContinue = true 446 | /* 447 | if bytes.Contains(s, []byte("100-continue")) { 448 | h.ExpectContinue = true 449 | } 450 | */ 451 | return nil 452 | } 453 | 454 | func splitHeader(s []byte) (name, val []byte, err error) { 455 | i := bytes.IndexByte(s, ':') 456 | if i < 0 { 457 | return nil, nil, fmt.Errorf("malformed header: %#v", string(s)) 458 | } 459 | // Do not lower case field value, as it maybe case sensitive 460 | return ASCIIToLower(s[:i]), TrimSpace(s[i+1:]), nil 461 | } 462 | 463 | // Learned from net.textproto. One difference is that this one keeps the 464 | // ending '\n' in the returned line. Buf if there's only CRLF in the line, 465 | // return nil for the line. 466 | func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) { 467 | // feedly.com request headers contains things like: 468 | // "$Authorization.feedly: $FeedlyAuth\r\n", so we must test for only 469 | // continuation spaces. 470 | isspace := func(b byte) bool { 471 | return b == ' ' || b == '\t' 472 | } 473 | 474 | // Read the first line. 475 | line, err := r.ReadSlice('\n') 476 | if err != nil { 477 | return nil, err 478 | } 479 | 480 | // There are servers that use \n for line ending, so trim first before check ending. 481 | // For example, the 404 page for http://plan9.bell-labs.com/magic/man2html/1/2l 482 | trimmed := TrimSpace(line) 483 | if len(trimmed) == 0 { 484 | if len(line) > 2 { 485 | return nil, fmt.Errorf("malformed end of headers, len: %d, %#v", len(line), string(line)) 486 | } 487 | return nil, nil 488 | } 489 | 490 | if isspace(line[0]) { 491 | return nil, fmt.Errorf("malformed header, start with space: %#v", string(line)) 492 | } 493 | 494 | // Optimistically assume that we have started to buffer the next line 495 | // and it starts with an ASCII letter (the next header key), so we can 496 | // avoid copying that buffered data around in memory and skipping over 497 | // non-existent whitespace. 498 | if r.Buffered() > 0 { 499 | peek, err := r.Peek(1) 500 | if err == nil && !isspace(peek[0]) { 501 | return line, nil 502 | } 503 | } 504 | 505 | var buf []byte 506 | buf = append(buf, trimmed...) 507 | 508 | // Read continuation lines. 509 | for skipSpace(r) > 0 { 510 | line, err := r.ReadSlice('\n') 511 | if err != nil { 512 | break 513 | } 514 | buf = append(buf, ' ') 515 | buf = append(buf, TrimTrailingSpace(line)...) 516 | } 517 | buf = append(buf, '\r', '\n') 518 | return buf, nil 519 | } 520 | 521 | func skipSpace(r *bufio.Reader) int { 522 | n := 0 523 | for { 524 | c, err := r.ReadByte() 525 | if err != nil { 526 | // Bufio will keep err until next read. 527 | break 528 | } 529 | if c != ' ' && c != '\t' { 530 | r.UnreadByte() 531 | break 532 | } 533 | n++ 534 | } 535 | return n 536 | } 537 | 538 | // Only add headers that are of interest for a proxy into request/response's header map. 539 | func (h *Header) parseHeader(reader *bufio.Reader, raw *bytes.Buffer, url *URL) (err error) { 540 | h.ContLen = -1 541 | for { 542 | var line, name, val []byte 543 | if line, err = readContinuedLineSlice(reader); err != nil || len(line) == 0 { 544 | return 545 | } 546 | if name, val, err = splitHeader(line); err != nil { 547 | errl.Printf("split header %v\nline: %s\nraw header:\n%s\n", err, line, raw.Bytes()) 548 | return 549 | } 550 | // Wait Go to solve/provide the string<->[]byte optimization 551 | kn := string(name) 552 | if parseFunc, ok := headerParser[kn]; ok { 553 | if len(val) == 0 { 554 | continue 555 | } 556 | if err = parseFunc(h, val); err != nil { 557 | errl.Printf("parse header %v\nline: %s\nraw header:\n%s\n", err, line, raw.Bytes()) 558 | return 559 | } 560 | } 561 | if hopByHopHeader[kn] { 562 | continue 563 | } 564 | raw.Write(line) 565 | // debug.Printf("len %d %s", len(s), s) 566 | } 567 | } 568 | 569 | // Parse the request line and header, does not touch body 570 | func parseRequest(c *clientConn, r *Request) (err error) { 571 | var s []byte 572 | reader := c.bufRd 573 | c.setReadTimeout("parseRequest") 574 | // parse request line 575 | if s, err = reader.ReadSlice('\n'); err != nil { 576 | if isErrTimeout(err) { 577 | return errClientTimeout 578 | } 579 | return err 580 | } 581 | c.unsetReadTimeout("parseRequest") 582 | // debug.Printf("Request line %s", s) 583 | 584 | r.reset() 585 | if config.saveReqLine { 586 | r.raw.Write(s) 587 | r.reqLnStart = len(s) 588 | } 589 | 590 | var f [][]byte 591 | // Tolerate with multiple spaces and '\t' is achieved by FieldsN. 592 | if f = FieldsN(s, 3); len(f) != 3 { 593 | return fmt.Errorf("malformed request line: %#v", string(s)) 594 | } 595 | ASCIIToUpperInplace(f[0]) 596 | r.Method = string(f[0]) 597 | 598 | // Parse URI into host and path 599 | r.URL, err = ParseRequestURIBytes(f[1]) 600 | if err != nil { 601 | return 602 | } 603 | r.Header.Host = r.URL.HostPort // If Header.Host is set, parseHost will just return. 604 | if r.Method == "CONNECT" { 605 | r.isConnect = true 606 | if bool(dbgRq) && verbose && !config.saveReqLine { 607 | r.raw.Write(s) 608 | } 609 | } else { 610 | r.genRequestLine() 611 | } 612 | r.headStart = r.raw.Len() 613 | 614 | // Read request header. 615 | if err = r.parseHeader(reader, r.raw, r.URL); err != nil { 616 | errl.Printf("parse request header: %v %s\n%s", err, r, r.Verbose()) 617 | return err 618 | } 619 | if r.Chunking { 620 | r.raw.WriteString(fullHeaderTransferEncoding) 621 | } 622 | if r.ConnectionKeepAlive { 623 | r.raw.WriteString(fullHeaderConnectionKeepAlive) 624 | } else { 625 | r.raw.WriteString(fullHeaderConnectionClose) 626 | } 627 | // The spec says proxy must add Via header. polipo disables this by 628 | // default, and I don't want to let others know the user is using meow, so 629 | // don't add it. 630 | r.raw.WriteString(CRLF) 631 | r.bodyStart = r.raw.Len() 632 | return 633 | } 634 | 635 | // If an http response may have message body 636 | func (rp *Response) hasBody(method string) bool { 637 | if method == "HEAD" || rp.Status == 304 || rp.Status == 204 || 638 | rp.Status < 200 { 639 | return false 640 | } 641 | return true 642 | } 643 | 644 | // Parse response status and headers. 645 | func parseResponse(sv *serverConn, r *Request, rp *Response) (err error) { 646 | var s []byte 647 | reader := sv.bufRd 648 | if s, err = reader.ReadSlice('\n'); err != nil { 649 | // err maybe timeout caused by explicity setting deadline, EOF, or 650 | // reset caused by GFW. 651 | debug.Printf("read response status line %v %v\n", err, r) 652 | // Server connection with error will not be used any more, so no need 653 | // to unset timeout. 654 | // For read error, return directly in order to identify whether this 655 | // is caused by GFW. 656 | return err 657 | } 658 | // debug.Printf("Response line %s", s) 659 | 660 | // response status line parsing 661 | var f [][]byte 662 | if f = FieldsN(s, 3); len(f) < 2 { // status line are separated by SP 663 | return fmt.Errorf("malformed response status line: %#v %v", string(s), r) 664 | } 665 | status, err := ParseIntFromBytes(f[1], 10) 666 | 667 | rp.reset() 668 | rp.Status = int(status) 669 | if err != nil { 670 | return fmt.Errorf("response status not valid: %s len=%d %v", f[1], len(f[1]), err) 671 | } 672 | if len(f) == 3 { 673 | rp.Reason = f[2] 674 | } 675 | 676 | proto := f[0] 677 | if !bytes.Equal(proto[0:7], []byte("HTTP/1.")) { 678 | return fmt.Errorf("invalid response status line: %s request %v", string(f[0]), r) 679 | } 680 | if proto[7] == '1' { 681 | rp.raw.Write(s) 682 | } else if proto[7] == '0' { 683 | // Should return HTTP version as 1.1 to client since closed connection 684 | // will be converted to chunked encoding 685 | rp.genStatusLine() 686 | } else { 687 | return fmt.Errorf("response protocol not supported: %s", f[0]) 688 | } 689 | 690 | if err = rp.parseHeader(reader, rp.raw, r.URL); err != nil { 691 | errl.Printf("parse response header: %v %s\n%s", err, r, rp.Verbose()) 692 | return err 693 | } 694 | 695 | //Check for http error code from config file 696 | if config.HttpErrorCode > 0 && rp.Status == config.HttpErrorCode { 697 | errl.Println("Requested http code is raised") 698 | return CustomHttpErr 699 | } 700 | 701 | if rp.Status == statusCodeContinue && !r.ExpectContinue { 702 | // not expecting 100-continue, just ignore it and read final response 703 | errl.Println("Ignore server 100 response for", r) 704 | return parseResponse(sv, r, rp) 705 | } 706 | 707 | if rp.Chunking { 708 | rp.raw.WriteString(fullHeaderTransferEncoding) 709 | } else if rp.ContLen == -1 { 710 | // No chunk, no content length, assume close to signal end. 711 | rp.ConnectionKeepAlive = false 712 | if rp.hasBody(r.Method) { 713 | // Connection close, no content length specification. 714 | // Use chunked encoding to pass content back to client. 715 | debug.Println("add chunked encoding to close connection response", r, rp) 716 | rp.raw.WriteString(fullHeaderTransferEncoding) 717 | } else if !(rp.Status == 304 || rp.Status == 204) { 718 | debug.Println("add content-length 0 to close connection response", r, rp) 719 | rp.raw.WriteString("Content-Length: 0\r\n") 720 | } 721 | } 722 | // Whether meow should respond with keep-alive depends on client request, 723 | // not server response. 724 | if r.ConnectionKeepAlive { 725 | rp.raw.WriteString(fullHeaderConnectionKeepAlive) 726 | rp.raw.WriteString(fullKeepAliveHeader) 727 | } else { 728 | rp.raw.WriteString(fullHeaderConnectionClose) 729 | } 730 | rp.raw.WriteString(CRLF) 731 | 732 | return nil 733 | } 734 | 735 | func unquote(s string) string { 736 | return strings.Trim(s, "\"") 737 | } 738 | 739 | func parseKeyValueList(str string) map[string]string { 740 | list := strings.Split(str, ",") 741 | if len(list) == 1 && list[0] == "" { 742 | return nil 743 | } 744 | res := make(map[string]string) 745 | for _, ele := range list { 746 | kv := strings.SplitN(strings.TrimSpace(ele), "=", 2) 747 | if len(kv) != 2 { 748 | errl.Println("no equal sign in key value list element:", ele) 749 | return nil 750 | } 751 | key, val := kv[0], unquote(kv[1]) 752 | res[key] = val 753 | } 754 | return res 755 | } 756 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/cyfdecyf/bufio" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestParseRequestURI(t *testing.T) { 12 | var testData = []struct { 13 | rawurl string 14 | url *URL 15 | }{ 16 | {"http://www.g.com", &URL{"www.g.com:80", "www.g.com", "80", "g.com", ""}}, 17 | {"http://plus.g.com/", &URL{"plus.g.com:80", "plus.g.com", "80", "g.com", "/"}}, 18 | {"https://g.com:80", &URL{"g.com:80", "g.com", "80", "g.com", ""}}, 19 | {"http://mail.g.com:80/", &URL{"mail.g.com:80", "mail.g.com", "80", "g.com", "/"}}, 20 | {"http://g.com:80/ncr", &URL{"g.com:80", "g.com", "80", "g.com", "/ncr"}}, 21 | {"https://g.com/ncr/tree", &URL{"g.com:443", "g.com", "443", "g.com", "/ncr/tree"}}, 22 | {"www.g.com.hk:80/", &URL{"www.g.com.hk:80", "www.g.com.hk", "80", "com.hk", "/"}}, 23 | {"g.com.jp:80", &URL{"g.com.jp:80", "g.com.jp", "80", "com.jp", ""}}, 24 | {"g.com", &URL{"g.com:80", "g.com", "80", "g.com", ""}}, 25 | {"g.com:8000/ncr", &URL{"g.com:8000", "g.com", "8000", "g.com", "/ncr"}}, 26 | {"g.com/ncr/tree", &URL{"g.com:80", "g.com", "80", "g.com", "/ncr/tree"}}, 27 | {"simplehost", &URL{"simplehost:80", "simplehost", "80", "", ""}}, 28 | {"simplehost:8080", &URL{"simplehost:8080", "simplehost", "8080", "", ""}}, 29 | {"192.168.1.1:8080/", &URL{"192.168.1.1:8080", "192.168.1.1", "8080", "", "/"}}, 30 | {"/helloworld", &URL{"", "", "", "", "/helloworld"}}, 31 | } 32 | for _, td := range testData { 33 | url, err := ParseRequestURI(td.rawurl) 34 | if url == nil { 35 | if err == nil { 36 | t.Error("nil URL must report error") 37 | } 38 | if td.url != nil { 39 | t.Error(td.rawurl, "should not report error") 40 | } 41 | continue 42 | } 43 | if err != nil { 44 | t.Error(td.rawurl, "non nil URL should not report error") 45 | } 46 | if url.HostPort != td.url.HostPort { 47 | t.Error(td.rawurl, "parsed hostPort wrong:", td.url.HostPort, "got", url.HostPort) 48 | } 49 | if url.Host != td.url.Host { 50 | t.Error(td.rawurl, "parsed host wrong:", td.url.Host, "got", url.Host) 51 | } 52 | if url.Port != td.url.Port { 53 | t.Error(td.rawurl, "parsed port wrong:", td.url.Port, "got", url.Port) 54 | } 55 | if url.Domain != td.url.Domain { 56 | t.Error(td.rawurl, "parsed domain wrong:", td.url.Domain, "got", url.Domain) 57 | } 58 | if url.Path != td.url.Path { 59 | t.Error(td.rawurl, "parsed path wrong:", td.url.Path, "got", url.Path) 60 | } 61 | } 62 | } 63 | 64 | func TestParseHeader(t *testing.T) { 65 | var testData = []struct { 66 | raw string 67 | newraw string 68 | header *Header 69 | }{ 70 | {"content-length: 64\r\nConnection: keep-alive\r\n\r\n", 71 | "content-length: 64\r\n", 72 | &Header{ContLen: 64, Chunking: false, ConnectionKeepAlive: true}}, 73 | {"Connection: keep-alive\r\nKeep-Alive: timeout=10\r\nTransfer-Encoding: chunked\r\nTE: trailers\r\n\r\n", 74 | "", 75 | &Header{ContLen: -1, Chunking: true, ConnectionKeepAlive: true, 76 | KeepAlive: 10 * time.Second}}, 77 | {"Connection:\r\n keep-alive\r\nKeep-Alive: max=5,\r\n timeout=10\r\n\r\n", 78 | "", 79 | &Header{ContLen: -1, Chunking: false, ConnectionKeepAlive: true, 80 | KeepAlive: 10 * time.Second}}, 81 | {"Connection: \r\n close\r\nLong: line\r\n continued\r\n\tagain\r\n\r\n", 82 | "Long: line continued again\r\n", 83 | &Header{ContLen: -1, Chunking: false, ConnectionKeepAlive: false}}, 84 | } 85 | for _, td := range testData { 86 | var h Header 87 | var newraw bytes.Buffer 88 | h.parseHeader(bufio.NewReader(strings.NewReader(td.raw)), &newraw, nil) 89 | if h.ContLen != td.header.ContLen { 90 | t.Errorf("%q parsed content length wrong, should be %d, get %d\n", 91 | td.raw, td.header.ContLen, h.ContLen) 92 | } 93 | if h.Chunking != td.header.Chunking { 94 | t.Errorf("%q parsed chunking wrong, should be %t, get %t\n", 95 | td.raw, td.header.Chunking, h.Chunking) 96 | } 97 | if h.ConnectionKeepAlive != td.header.ConnectionKeepAlive { 98 | t.Errorf("%q parsed connection wrong, should be %v, get %v\n", 99 | td.raw, td.header.ConnectionKeepAlive, h.ConnectionKeepAlive) 100 | } 101 | if h.KeepAlive != td.header.KeepAlive { 102 | t.Errorf("%q parsed keep alive wrong, should be %v, get %v\n", 103 | td.raw, td.header.KeepAlive, h.KeepAlive) 104 | } 105 | if newraw.String() != td.newraw { 106 | t.Errorf("%q parsed raw wrong\nshould be: %q\ngot: %q\n", 107 | td.raw, td.newraw, newraw.Bytes()) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=1.5 4 | 5 | arch=`uname -m` 6 | case $arch in 7 | "x86_64") 8 | arch="amd64" 9 | ;; 10 | "i386" | "i586" | "i486" | "i686") 11 | arch="386" 12 | ;; 13 | "armv5tel" | "armv6l" | "armv7l") 14 | arch="arm" 15 | ;; 16 | *) 17 | echo "$arch currently has no precompiled binary" 18 | ;; 19 | esac 20 | 21 | os=`uname -s` 22 | case $os in 23 | "Darwin") 24 | os="darwin" 25 | ;; 26 | "Linux") 27 | os="linux" 28 | ;; 29 | *) 30 | echo "$os currently has no precompiled binary" 31 | exit 1 32 | esac 33 | 34 | exit_on_fail() { 35 | if [ $? != 0 ]; then 36 | echo $1 37 | exit 1 38 | fi 39 | } 40 | 41 | while true; do 42 | # Get install directory from environment variable. 43 | if [[ -n $MEOW_INSTALLDIR && -d $MEOW_INSTALLDIR ]]; then 44 | install_dir=$MEOW_INSTALLDIR 45 | break 46 | fi 47 | 48 | # Get installation directory from user 49 | echo -n "Install MEOW binary to which directory (absolute path, defaults to current dir): " 50 | read install_dir $la_dir/$plist || \ 119 | exit_on_fail "Download startup plist file to $la_dir failed" 120 | fi 121 | 122 | # Move binary to install directory 123 | echo "Move $tmpbin to $install_dir (will run sudo if no write permission to install directory)" 124 | if [ -w $install_dir ]; then 125 | mv $tmpbin $install_dir 126 | else 127 | sudo mv $tmpbin $install_dir 128 | fi 129 | exit_on_fail "Failed to move $tmpbin to $install_dir" 130 | rmdir $tmpdir 131 | 132 | # Done 133 | echo 134 | if $is_update; then 135 | echo "Update finished." 136 | else 137 | echo "Installation finished." 138 | echo "Please edit $config_dir/rc according to your own settings." 139 | echo 'After that, execute "MEOW &" to start MEOW and run in background.' 140 | fi 141 | 142 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This logging trick is learnt from a post by Rob Pike 4 | // https://groups.google.com/d/msg/golang-nuts/gU7oQGoCkmg/j3nNxuS2O_sJ 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | 13 | "github.com/cyfdecyf/color" 14 | ) 15 | 16 | type infoLogging bool 17 | type debugLogging bool 18 | type errorLogging bool 19 | type requestLogging bool 20 | type responseLogging bool 21 | 22 | var ( 23 | info infoLogging 24 | debug debugLogging 25 | errl errorLogging 26 | dbgRq requestLogging 27 | dbgRep responseLogging 28 | 29 | logFile io.Writer 30 | 31 | // make sure logger can be called before initLog 32 | errorLog = log.New(os.Stdout, "[ERROR] ", log.LstdFlags) 33 | debugLog = log.New(os.Stdout, "[DEBUG] ", log.LstdFlags) 34 | requestLog = log.New(os.Stdout, "[>>>>>] ", log.LstdFlags) 35 | responseLog = log.New(os.Stdout, "[<<<<<] ", log.LstdFlags) 36 | 37 | verbose bool 38 | colorize bool 39 | ) 40 | 41 | func init() { 42 | flag.BoolVar((*bool)(&info), "info", true, "info log") 43 | flag.BoolVar((*bool)(&debug), "debug", false, "debug log, with this option, log goes to stdout with color") 44 | flag.BoolVar((*bool)(&errl), "err", true, "error log") 45 | flag.BoolVar((*bool)(&dbgRq), "request", true, "request log") 46 | flag.BoolVar((*bool)(&dbgRep), "reply", true, "reply log") 47 | flag.BoolVar(&verbose, "v", false, "more info in request/response logging") 48 | flag.BoolVar(&colorize, "color", false, "colorize log output") 49 | } 50 | 51 | func initLog() { 52 | logFile = os.Stdout 53 | if config.LogFile != "" { 54 | if f, err := os.OpenFile(expandTilde(config.LogFile), 55 | os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err != nil { 56 | fmt.Printf("Can't open log file, logging to stdout: %v\n", err) 57 | } else { 58 | logFile = f 59 | } 60 | } 61 | log.SetOutput(logFile) 62 | if colorize { 63 | color.SetDefaultColor(color.ANSI) 64 | } else { 65 | color.SetDefaultColor(color.NoColor) 66 | } 67 | errorLog = log.New(logFile, color.Red("[ERROR] "), log.LstdFlags) 68 | debugLog = log.New(logFile, color.Blue("[DEBUG] "), log.LstdFlags) 69 | requestLog = log.New(logFile, color.Green("[>>>>>] "), log.LstdFlags) 70 | responseLog = log.New(logFile, color.Yellow("[<<<<<] "), log.LstdFlags) 71 | } 72 | 73 | func (d infoLogging) Printf(format string, args ...interface{}) { 74 | if d { 75 | log.Printf(format, args...) 76 | } 77 | } 78 | 79 | func (d infoLogging) Println(args ...interface{}) { 80 | if d { 81 | log.Println(args...) 82 | } 83 | } 84 | 85 | func (d debugLogging) Printf(format string, args ...interface{}) { 86 | if d { 87 | debugLog.Printf(format, args...) 88 | } 89 | } 90 | 91 | func (d debugLogging) Println(args ...interface{}) { 92 | if d { 93 | debugLog.Println(args...) 94 | } 95 | } 96 | 97 | func (d errorLogging) Printf(format string, args ...interface{}) { 98 | if d { 99 | errorLog.Printf(format, args...) 100 | } 101 | } 102 | 103 | func (d errorLogging) Println(args ...interface{}) { 104 | if d { 105 | errorLog.Println(args...) 106 | } 107 | } 108 | 109 | func (d requestLogging) Printf(format string, args ...interface{}) { 110 | if d { 111 | requestLog.Printf(format, args...) 112 | } 113 | } 114 | 115 | func (d responseLogging) Printf(format string, args ...interface{}) { 116 | if d { 117 | responseLog.Printf(format, args...) 118 | } 119 | } 120 | 121 | func Fatal(args ...interface{}) { 122 | fmt.Println(args...) 123 | os.Exit(1) 124 | } 125 | 126 | func Fatalf(format string, args ...interface{}) { 127 | fmt.Printf(format, args...) 128 | os.Exit(1) 129 | } 130 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "sync" 8 | ) 9 | 10 | func main() { 11 | // Parse flags after load config to allow override options in config 12 | cmdLineConfig := parseCmdLineConfig() 13 | if cmdLineConfig.PrintVer { 14 | printVersion() 15 | os.Exit(0) 16 | } 17 | 18 | fmt.Printf(` 19 | /\ 20 | ) ( ') MEOW Proxy %s 21 | ( / ) http://renzhn.github.io/MEOW/ 22 | \(__)| 23 | `, version) 24 | fmt.Println() 25 | 26 | parseConfig(cmdLineConfig.RcFile, cmdLineConfig) 27 | 28 | initSelfListenAddr() 29 | initLog() 30 | initAuth() 31 | initStat() 32 | 33 | initParentPool() 34 | 35 | if config.JudgeByIP { 36 | initCNIPData() 37 | } 38 | 39 | if config.Core > 0 { 40 | runtime.GOMAXPROCS(config.Core) 41 | } 42 | 43 | go runSSH() 44 | 45 | var wg sync.WaitGroup 46 | wg.Add(len(listenProxy)) 47 | for _, proxy := range listenProxy { 48 | go proxy.Serve(&wg) 49 | } 50 | wg.Wait() 51 | } 52 | -------------------------------------------------------------------------------- /pac.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "text/template" 8 | ) 9 | 10 | var pac struct { 11 | template *template.Template 12 | } 13 | 14 | func init() { 15 | const pacRawTmpl = `var direct = 'DIRECT'; 16 | var httpProxy = 'PROXY {{.ProxyAddr}}; DIRECT'; 17 | 18 | var domainList = [ 19 | "{{.DirectDomains}}" 20 | ]; 21 | 22 | var directAcc = []; 23 | for (var i = 0; i < domainList.length; i += 1) { 24 | directAcc[domainList[i]] = true; 25 | } 26 | 27 | // hostIsIP determines whether a host address is an IP address and whether 28 | // it is private. Currenly only handles IPv4 addresses. 29 | function hostIsIP(host) { 30 | var part = host.split('.'); 31 | if (part.length != 4) { 32 | return [false, false]; 33 | } 34 | var n; 35 | for (var i = 3; i >= 0; i--) { 36 | if (part[i].length === 0 || part[i].length > 3) { 37 | return [false, false]; 38 | } 39 | n = Number(part[i]); 40 | if (isNaN(n) || n < 0 || n > 255) { 41 | return [false, false]; 42 | } 43 | } 44 | if (part[0] == '127' || part[0] == '10' || (part[0] == '192' && part[1] == '168')) { 45 | return [true, true]; 46 | } 47 | if (part[0] == '172') { 48 | n = Number(part[1]); 49 | if (16 <= n && n <= 31) { 50 | return [true, true]; 51 | } 52 | } 53 | return [true, false]; 54 | } 55 | 56 | function host2Domain(host) { 57 | var arr, isIP, isPrivate; 58 | arr = hostIsIP(host); 59 | isIP = arr[0]; 60 | isPrivate = arr[1]; 61 | if (isPrivate) { 62 | return ""; 63 | } 64 | if (isIP) { 65 | return host; 66 | } 67 | 68 | var lastDot = host.lastIndexOf('.'); 69 | if (lastDot === -1) { 70 | return ""; // simple host name has no domain 71 | } 72 | // Find the second last dot 73 | dot2ndLast = host.lastIndexOf(".", lastDot-1); 74 | if (dot2ndLast === -1) 75 | return host; 76 | 77 | return host.substring(dot2ndLast+1); 78 | } 79 | 80 | function FindProxyForURL(url, host) { 81 | if (url.substring(0,4) == "ftp:") 82 | return direct; 83 | if (host.substring(0,7) == "::ffff:") 84 | return direct; 85 | if (host.indexOf(".local", host.length - 6) !== -1) { 86 | return direct; 87 | } 88 | return (directAcc[host] || directAcc[host2Domain(host)]) ? direct : httpProxy; 89 | } 90 | ` 91 | var err error 92 | pac.template, err = template.New("pac").Parse(pacRawTmpl) 93 | if err != nil { 94 | Fatal("Internal error on generating pac file template:", err) 95 | } 96 | 97 | } 98 | 99 | // No need for content-length as we are closing connection 100 | var pacHeader = []byte("HTTP/1.1 200 OK\r\nServer: meow-proxy\r\n" + 101 | "Content-Type: application/x-ns-proxy-autoconfig\r\nConnection: close\r\n\r\n") 102 | 103 | // Different client will have different proxy URL, so generate it upon each request. 104 | func genPAC(c *clientConn) []byte { 105 | buf := new(bytes.Buffer) 106 | 107 | hproxy, ok := c.proxy.(*httpProxy) 108 | if !ok { 109 | panic("sendPAC should only be called for http proxy") 110 | } 111 | 112 | proxyAddr := hproxy.addrInPAC 113 | if proxyAddr == "" { 114 | host, _, err := net.SplitHostPort(c.LocalAddr().String()) 115 | // This is the only check to split host port on tcp addr's string 116 | // representation in meow. Keep it so we will notice if there's any 117 | // problem in the future. 118 | if err != nil { 119 | panic("split host port on local address error") 120 | } 121 | proxyAddr = net.JoinHostPort(host, hproxy.port) 122 | } 123 | 124 | directDomains := "" 125 | for k, v := range domainList.Domain { 126 | if v == domainTypeDirect { 127 | directDomains += k + "\",\n\"" 128 | } 129 | } 130 | 131 | if directDomains == "" { 132 | // Empty direct domain list 133 | buf.Write(pacHeader) 134 | pacproxy := fmt.Sprintf("function FindProxyForURL(url, host) { return 'PROXY %s; DIRECT'; };", 135 | proxyAddr) 136 | buf.Write([]byte(pacproxy)) 137 | return buf.Bytes() 138 | } 139 | 140 | data := struct { 141 | ProxyAddr string 142 | DirectDomains string 143 | }{ 144 | proxyAddr, 145 | directDomains, 146 | } 147 | 148 | buf.Write(pacHeader) 149 | if err := pac.template.Execute(buf, data); err != nil { 150 | errl.Println("Error generating pac file:", err) 151 | panic("Error generating pac file") 152 | } 153 | return buf.Bytes() 154 | } 155 | 156 | func sendPAC(c *clientConn) error { 157 | _, err := c.Write(genPAC(c)) 158 | if err != nil { 159 | debug.Printf("cli(%s) error sending PAC: %s", c.RemoteAddr(), err) 160 | } 161 | return err 162 | } 163 | -------------------------------------------------------------------------------- /pac.js: -------------------------------------------------------------------------------- 1 | var direct = 'DIRECT'; 2 | var httpProxy = 'PROXY'; 3 | 4 | var directList = [ 5 | "baidu.com", 6 | "www.taobao.com" 7 | "", // corresponds to simple host name and ip address 8 | ]; 9 | 10 | var directAcc = {}; 11 | for (var i = 0; i < directList.length; i += 1) { 12 | directAcc[directList[i]] = true; 13 | } 14 | 15 | // hostIsIP determines whether a host address is an IP address and whether 16 | // it is private. Currenly only handles IPv4 addresses. 17 | function hostIsIP(host) { 18 | var part = host.split('.'); 19 | if (part.length != 4) { 20 | return [false, false]; 21 | } 22 | var n; 23 | for (var i = 3; i >= 0; i--) { 24 | if (part[i].length === 0 || part[i].length > 3) { 25 | return [false, false]; 26 | } 27 | n = Number(part[i]); 28 | if (isNaN(n) || n < 0 || n > 255) { 29 | return [false, false]; 30 | } 31 | } 32 | if (part[0] == '127' || part[0] == '10' || (part[0] == '192' && part[1] == '168')) { 33 | return [true, true]; 34 | } 35 | if (part[0] == '172') { 36 | n = Number(part[1]); 37 | if (16 <= n && n <= 31) { 38 | return [true, true]; 39 | } 40 | } 41 | return [true, false]; 42 | } 43 | 44 | function host2Domain(host) { 45 | var arr, isIP, isPrivate; 46 | arr = hostIsIP(host); 47 | isIP = arr[0]; 48 | isPrivate = arr[1]; 49 | if (isPrivate) { 50 | return ""; 51 | } 52 | if (isIP) { 53 | return host; 54 | } 55 | 56 | var lastDot = host.lastIndexOf('.'); 57 | if (lastDot === -1) { 58 | return ""; // simple host name has no domain 59 | } 60 | // Find the second last dot 61 | dot2ndLast = host.lastIndexOf(".", lastDot-1); 62 | if (dot2ndLast === -1) 63 | return host; 64 | 65 | return host.substring(dot2ndLast+1); 66 | } 67 | 68 | function FindProxyForURL(url, host) { 69 | if (url.substring(0,4) == "ftp:") 70 | return direct; 71 | if (host.indexOf(".local", host.length - 6) !== -1) { 72 | return direct; 73 | } 74 | return (directAcc[host] || directAcc[host2Domain(host)]) ? direct : httpProxy; 75 | } 76 | 77 | // Tests 78 | 79 | var testData, td, i; 80 | 81 | testData = [ 82 | { ip: '127.0.0.1', isIP: true, isPrivate: true }, 83 | { ip: '127.2.1.1', isIP: true, isPrivate: true }, 84 | { ip: '192.168.1.1', isIP: true, isPrivate: true }, 85 | { ip: '172.16.1.1', isIP: true, isPrivate: true }, 86 | { ip: '172.20.1.1', isIP: true, isPrivate: true }, 87 | { ip: '172.31.1.1', isIP: true, isPrivate: true }, 88 | { ip: '172.15.1.1', isIP: true, isPrivate: false }, 89 | { ip: '172.32.1.1', isIP: true, isPrivate: false }, 90 | { ip: '10.16.1.1', isIP: true, isPrivate: true }, 91 | { ip: '12.3.4.5', isIP: true, isPrivate: false }, 92 | { ip: '1.2.3.4.5', isIP: false, isPrivate: false }, 93 | { ip: 'google.com', isIP: false, isPrivate: false }, 94 | { ip: 'www.google.com.hk', isIP: false, isPrivate: false } 95 | ]; 96 | 97 | for (i = 0; i < testData.length; i += 1) { 98 | td = testData[i]; 99 | arr = hostIsIP(td.ip); 100 | if (arr[0] !== td.isIP) { 101 | if (td.isIP) { 102 | console.log(td.ip + " is ip"); 103 | } else { 104 | console.log(td.ip + " is NOT ip"); 105 | } 106 | } 107 | if (arr[0] !== td.isIP) { 108 | if (td.isIP) { 109 | console.log(td.ip + " is private ip"); 110 | } else { 111 | console.log(td.ip + " is NOT private ip"); 112 | } 113 | } 114 | } 115 | 116 | testData = [ 117 | // private ip should return direct 118 | { host: '192.168.1.1', mode: direct}, 119 | { host: '10.1.1.1', mode: direct}, 120 | { host: '172.16.2.1', mode: direct}, 121 | { host: '172.20.255.255', mode: direct}, 122 | { host: '172.31.255.255', mode: direct}, 123 | { host: '192.168.2.255', mode: direct}, 124 | 125 | // simple host should return direct 126 | { host: 'localhost', mode: direct}, 127 | { host: 'simple', mode: direct}, 128 | 129 | // non private ip should return proxy 130 | { host: '172.32.2.255', mode: httpProxy}, 131 | { host: '172.15.0.255', mode: httpProxy}, 132 | { host: '12.20.2.1', mode: httpProxy}, 133 | 134 | // host in direct domain/host should return direct 135 | { host: 'baidu.com', mode: direct}, 136 | { host: 'www.baidu.com', mode: direct}, 137 | { host: 'www.taobao.com', mode: direct}, 138 | 139 | // host not in direct domain should return proxy 140 | { host: 'taobao.com', mode: httpProxy}, 141 | { host: 'foo.taobao.com', mode: httpProxy}, 142 | { host: 'google.com', mode: httpProxy}, 143 | { host: 'www.google.com', mode: httpProxy}, 144 | { host: 'www.google.com.hk', mode: httpProxy}, 145 | 146 | // host in local domain should return direct 147 | { host: 'test.local', mode: direct}, 148 | { host: '.local', mode: direct}, 149 | ]; 150 | 151 | for (i = 0; i < testData.length; i += 1) { 152 | td = testData[i]; 153 | if (FindProxyForURL("", td.host) !== td.mode) { 154 | if (td.mode === direct) { 155 | console.log(td.host + " should return direct"); 156 | } else { 157 | console.log(td.host + " should return proxy"); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /parent_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "hash/crc32" 10 | "io" 11 | "math/rand" 12 | "net" 13 | "sort" 14 | "strconv" 15 | "sync" 16 | "time" 17 | 18 | ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" 19 | ) 20 | 21 | // Interface that all types of parent proxies should support. 22 | type ParentProxy interface { 23 | connect(*URL) (net.Conn, error) 24 | getServer() string // for use in updating server latency 25 | genConfig() string // for upgrading config 26 | } 27 | 28 | // Interface for different proxy selection strategy. 29 | type ParentPool interface { 30 | add(ParentProxy) 31 | empty() bool 32 | // Select a proxy from the pool and connect. May try several proxies until 33 | // one that succees, return nil and error if all parent proxies fail. 34 | connect(*URL) (net.Conn, error) 35 | } 36 | 37 | // Init parentProxy to be backup pool. So config parsing have a pool to add 38 | // parent proxies. 39 | var parentProxy ParentPool = &backupParentPool{} 40 | 41 | func initParentPool() { 42 | backPool, ok := parentProxy.(*backupParentPool) 43 | if !ok { 44 | panic("initial parent pool should be backup pool") 45 | } 46 | if debug { 47 | printParentProxy(backPool.parent) 48 | } 49 | if len(backPool.parent) == 0 { 50 | info.Println("no parent proxy server") 51 | return 52 | } 53 | if len(backPool.parent) == 1 && config.LoadBalance != loadBalanceBackup { 54 | debug.Println("only 1 parent, no need for load balance") 55 | config.LoadBalance = loadBalanceBackup 56 | } 57 | 58 | switch config.LoadBalance { 59 | case loadBalanceHash: 60 | debug.Println("hash parent pool", len(backPool.parent)) 61 | parentProxy = &hashParentPool{*backPool} 62 | case loadBalanceLatency: 63 | debug.Println("latency parent pool", len(backPool.parent)) 64 | go updateParentProxyLatency() 65 | parentProxy = newLatencyParentPool(backPool.parent) 66 | } 67 | } 68 | 69 | func printParentProxy(parent []ParentWithFail) { 70 | debug.Println("avaiable parent proxies:") 71 | for _, pp := range parent { 72 | switch pc := pp.ParentProxy.(type) { 73 | case *shadowsocksParent: 74 | debug.Println("\tshadowsocks: ", pc.server) 75 | case *httpParent: 76 | debug.Println("\thttp parent: ", pc.server) 77 | case *socksParent: 78 | debug.Println("\tsocks parent: ", pc.server) 79 | case *meowParent: 80 | debug.Println("\tmeow parent: ", pc.server) 81 | } 82 | } 83 | } 84 | 85 | type ParentWithFail struct { 86 | ParentProxy 87 | fail int 88 | } 89 | 90 | // Backup load balance strategy: 91 | // Select proxy in the order they appear in config. 92 | type backupParentPool struct { 93 | parent []ParentWithFail 94 | } 95 | 96 | func (pp *backupParentPool) empty() bool { 97 | return len(pp.parent) == 0 98 | } 99 | 100 | func (pp *backupParentPool) add(parent ParentProxy) { 101 | pp.parent = append(pp.parent, ParentWithFail{parent, 0}) 102 | } 103 | 104 | func (pp *backupParentPool) connect(url *URL) (srvconn net.Conn, err error) { 105 | return connectInOrder(url, pp.parent, 0) 106 | } 107 | 108 | // Hash load balance strategy: 109 | // Each host will use a proxy based on a hash value. 110 | type hashParentPool struct { 111 | backupParentPool 112 | } 113 | 114 | func (pp *hashParentPool) connect(url *URL) (srvconn net.Conn, err error) { 115 | start := int(crc32.ChecksumIEEE([]byte(url.Host)) % uint32(len(pp.parent))) 116 | debug.Printf("hash host %s try %d parent first", url.Host, start) 117 | return connectInOrder(url, pp.parent, start) 118 | } 119 | 120 | func (parent *ParentWithFail) connect(url *URL) (srvconn net.Conn, err error) { 121 | const maxFailCnt = 30 122 | srvconn, err = parent.ParentProxy.connect(url) 123 | if err != nil { 124 | if parent.fail < maxFailCnt { 125 | parent.fail++ 126 | } 127 | return 128 | } 129 | parent.fail = 0 130 | return 131 | } 132 | 133 | func connectInOrder(url *URL, pp []ParentWithFail, start int) (srvconn net.Conn, err error) { 134 | const baseFailCnt = 9 135 | var skipped []int 136 | nproxy := len(pp) 137 | 138 | if nproxy == 0 { 139 | return nil, errors.New("no parent proxy") 140 | } 141 | 142 | for i := 0; i < nproxy; i++ { 143 | proxyId := (start + i) % nproxy 144 | parent := &pp[proxyId] 145 | // skip failed server, but try it with some probability 146 | if parent.fail > 0 && rand.Intn(parent.fail+baseFailCnt) != 0 { 147 | skipped = append(skipped, proxyId) 148 | continue 149 | } 150 | if srvconn, err = parent.connect(url); err == nil { 151 | return 152 | } 153 | } 154 | // last resort, try skipped one, not likely to succeed 155 | for _, skippedId := range skipped { 156 | if srvconn, err = pp[skippedId].connect(url); err == nil { 157 | return 158 | } 159 | } 160 | return nil, err 161 | } 162 | 163 | type ParentWithLatency struct { 164 | ParentProxy 165 | latency time.Duration 166 | } 167 | 168 | type latencyParentPool struct { 169 | parent []ParentWithLatency 170 | } 171 | 172 | func newLatencyParentPool(parent []ParentWithFail) *latencyParentPool { 173 | lp := &latencyParentPool{} 174 | for _, p := range parent { 175 | lp.add(p.ParentProxy) 176 | } 177 | return lp 178 | } 179 | 180 | func (pp *latencyParentPool) empty() bool { 181 | return len(pp.parent) == 0 182 | } 183 | 184 | func (pp *latencyParentPool) add(parent ParentProxy) { 185 | pp.parent = append(pp.parent, ParentWithLatency{parent, 0}) 186 | } 187 | 188 | // Sort interface. 189 | func (pp *latencyParentPool) Len() int { 190 | return len(pp.parent) 191 | } 192 | 193 | func (pp *latencyParentPool) Swap(i, j int) { 194 | p := pp.parent 195 | p[i], p[j] = p[j], p[i] 196 | } 197 | 198 | func (pp *latencyParentPool) Less(i, j int) bool { 199 | p := pp.parent 200 | return p[i].latency < p[j].latency 201 | } 202 | 203 | const latencyMax = time.Hour 204 | 205 | var latencyMutex sync.RWMutex 206 | 207 | func (pp *latencyParentPool) connect(url *URL) (srvconn net.Conn, err error) { 208 | var lp []ParentWithLatency 209 | // Read slice first. 210 | latencyMutex.RLock() 211 | lp = pp.parent 212 | latencyMutex.RUnlock() 213 | 214 | var skipped []int 215 | nproxy := len(lp) 216 | if nproxy == 0 { 217 | return nil, errors.New("no parent proxy") 218 | } 219 | 220 | for i := 0; i < nproxy; i++ { 221 | parent := lp[i] 222 | if parent.latency >= latencyMax { 223 | skipped = append(skipped, i) 224 | continue 225 | } 226 | if srvconn, err = parent.connect(url); err == nil { 227 | debug.Println("lowest latency proxy", parent.getServer()) 228 | return 229 | } 230 | parent.latency = latencyMax 231 | } 232 | // last resort, try skipped one, not likely to succeed 233 | for _, skippedId := range skipped { 234 | if srvconn, err = lp[skippedId].connect(url); err == nil { 235 | return 236 | } 237 | } 238 | return nil, err 239 | } 240 | 241 | func (parent *ParentWithLatency) updateLatency(wg *sync.WaitGroup) { 242 | defer wg.Done() 243 | proxy := parent.ParentProxy 244 | server := proxy.getServer() 245 | 246 | host, port, err := net.SplitHostPort(server) 247 | if err != nil { 248 | panic("split host port parent server error" + err.Error()) 249 | } 250 | 251 | // Resolve host name first, so latency does not include resolve time. 252 | ip, err := net.LookupIP(host) 253 | if err != nil { 254 | parent.latency = latencyMax 255 | return 256 | } 257 | ipPort := net.JoinHostPort(ip[0].String(), port) 258 | 259 | const N = 3 260 | var total time.Duration 261 | for i := 0; i < N; i++ { 262 | now := time.Now() 263 | cn, err := net.Dial("tcp", ipPort) 264 | if err != nil { 265 | debug.Println("latency update dial:", err) 266 | total += time.Minute // 1 minute as penalty 267 | continue 268 | } 269 | total += time.Now().Sub(now) 270 | cn.Close() 271 | 272 | time.Sleep(5 * time.Millisecond) 273 | } 274 | parent.latency = total / N 275 | debug.Println("latency", server, parent.latency) 276 | } 277 | 278 | func (pp *latencyParentPool) updateLatency() { 279 | // Create a copy, update latency for the copy. 280 | var cp latencyParentPool 281 | cp.parent = append(cp.parent, pp.parent...) 282 | 283 | // cp.parent is value instead of pointer, if we use `_, p := range cp.parent`, 284 | // the value in cp.parent will not be updated. 285 | var wg sync.WaitGroup 286 | wg.Add(len(cp.parent)) 287 | for i, _ := range cp.parent { 288 | cp.parent[i].updateLatency(&wg) 289 | } 290 | wg.Wait() 291 | 292 | // Sort according to latency. 293 | sort.Stable(&cp) 294 | debug.Println("latency lowest proxy", cp.parent[0].getServer()) 295 | 296 | // Update parent slice. 297 | latencyMutex.Lock() 298 | pp.parent = cp.parent 299 | latencyMutex.Unlock() 300 | } 301 | 302 | func updateParentProxyLatency() { 303 | lp, ok := parentProxy.(*latencyParentPool) 304 | if !ok { 305 | return 306 | } 307 | 308 | for { 309 | lp.updateLatency() 310 | time.Sleep(60 * time.Second) 311 | } 312 | } 313 | 314 | type httpsParent struct { 315 | server string 316 | userPasswd string // for upgrade config 317 | authHeader []byte 318 | } 319 | 320 | type httpsConn struct { 321 | net.Conn 322 | parent *httpsParent 323 | } 324 | 325 | func (s httpsConn) String() string { 326 | return "https parent proxy " + s.parent.server 327 | } 328 | 329 | func newHttpsParent(server string) *httpsParent { 330 | return &httpsParent{server: server} 331 | } 332 | 333 | func (hp *httpsParent) getServer() string { 334 | return hp.server 335 | } 336 | 337 | func (hp *httpsParent) genConfig() string { 338 | if hp.userPasswd != "" { 339 | return fmt.Sprintf("proxy = https://%s@%s", hp.userPasswd, hp.server) 340 | } else { 341 | return fmt.Sprintf("proxy = https://%s", hp.server) 342 | } 343 | } 344 | 345 | func (hp *httpsParent) initAuth(userPasswd string) { 346 | if userPasswd == "" { 347 | return 348 | } 349 | hp.userPasswd = userPasswd 350 | b64 := base64.StdEncoding.EncodeToString([]byte(userPasswd)) 351 | hp.authHeader = []byte(headerProxyAuthorization + ": Basic " + b64 + CRLF) 352 | } 353 | 354 | func (hp *httpsParent) connect(url *URL) (net.Conn, error) { 355 | c, err := tls.Dial("tcp", hp.server, &tls.Config{ 356 | InsecureSkipVerify: true, 357 | }) 358 | if err != nil { 359 | errl.Printf("can't connect to https parent %s for %s: %v\n", 360 | hp.server, url.HostPort, err) 361 | return nil, err 362 | } 363 | 364 | debug.Printf("connected to: %s via https parent: %s\n", 365 | url.HostPort, hp.server) 366 | return httpsConn{c, hp}, nil 367 | } 368 | 369 | // http parent proxy 370 | type httpParent struct { 371 | server string 372 | userPasswd string // for upgrade config 373 | authHeader []byte 374 | } 375 | 376 | type httpConn struct { 377 | net.Conn 378 | parent *httpParent 379 | } 380 | 381 | func (s httpConn) String() string { 382 | return "http parent proxy " + s.parent.server 383 | } 384 | 385 | func newHttpParent(server string) *httpParent { 386 | return &httpParent{server: server} 387 | } 388 | 389 | func (hp *httpParent) getServer() string { 390 | return hp.server 391 | } 392 | 393 | func (hp *httpParent) genConfig() string { 394 | if hp.userPasswd != "" { 395 | return fmt.Sprintf("proxy = http://%s@%s", hp.userPasswd, hp.server) 396 | } else { 397 | return fmt.Sprintf("proxy = http://%s", hp.server) 398 | } 399 | } 400 | 401 | func (hp *httpParent) initAuth(userPasswd string) { 402 | if userPasswd == "" { 403 | return 404 | } 405 | hp.userPasswd = userPasswd 406 | b64 := base64.StdEncoding.EncodeToString([]byte(userPasswd)) 407 | hp.authHeader = []byte(headerProxyAuthorization + ": Basic " + b64 + CRLF) 408 | } 409 | 410 | func (hp *httpParent) connect(url *URL) (net.Conn, error) { 411 | c, err := net.Dial("tcp", hp.server) 412 | if err != nil { 413 | errl.Printf("can't connect to http parent %s for %s: %v\n", 414 | hp.server, url.HostPort, err) 415 | return nil, err 416 | } 417 | debug.Printf("connected to: %s via http parent: %s\n", 418 | url.HostPort, hp.server) 419 | return httpConn{c, hp}, nil 420 | } 421 | 422 | // shadowsocks parent proxy 423 | type shadowsocksParent struct { 424 | server string 425 | method string // method and passwd are for upgrade config 426 | passwd string 427 | cipher *ss.Cipher 428 | } 429 | 430 | type shadowsocksConn struct { 431 | net.Conn 432 | parent *shadowsocksParent 433 | } 434 | 435 | func (s shadowsocksConn) String() string { 436 | return "shadowsocks proxy " + s.parent.server 437 | } 438 | 439 | // In order to use parent proxy in the order specified in the config file, we 440 | // insert an uninitialized proxy into parent proxy list, and initialize it 441 | // when all its config have been parsed. 442 | 443 | func newShadowsocksParent(server string) *shadowsocksParent { 444 | return &shadowsocksParent{server: server} 445 | } 446 | 447 | func (sp *shadowsocksParent) getServer() string { 448 | return sp.server 449 | } 450 | 451 | func (sp *shadowsocksParent) genConfig() string { 452 | method := sp.method 453 | if method == "" { 454 | method = "table" 455 | } 456 | return fmt.Sprintf("proxy = ss://%s:%s@%s", method, sp.passwd, sp.server) 457 | } 458 | 459 | func (sp *shadowsocksParent) initCipher(method, passwd string) { 460 | sp.method = method 461 | sp.passwd = passwd 462 | cipher, err := ss.NewCipher(method, passwd) 463 | if err != nil { 464 | Fatal("create shadowsocks cipher:", err) 465 | } 466 | sp.cipher = cipher 467 | } 468 | 469 | func (sp *shadowsocksParent) connect(url *URL) (net.Conn, error) { 470 | c, err := ss.Dial(url.HostPort, sp.server, sp.cipher.Copy()) 471 | if err != nil { 472 | errl.Printf("can't connect to shadowsocks parent %s for %s: %v\n", 473 | sp.server, url.HostPort, err) 474 | return nil, err 475 | } 476 | debug.Println("connected to:", url.HostPort, "via shadowsocks:", sp.server) 477 | return shadowsocksConn{c, sp}, nil 478 | } 479 | 480 | // meow parent proxy 481 | type meowParent struct { 482 | server string 483 | method string 484 | passwd string 485 | cipher *ss.Cipher 486 | } 487 | 488 | type meowConn struct { 489 | net.Conn 490 | parent *meowParent 491 | } 492 | 493 | func (s meowConn) String() string { 494 | return "meow proxy " + s.parent.server 495 | } 496 | 497 | func newMeowParent(srv, method, passwd string) *meowParent { 498 | cipher, err := ss.NewCipher(method, passwd) 499 | if err != nil { 500 | Fatal("create meow cipher:", err) 501 | } 502 | return &meowParent{srv, method, passwd, cipher} 503 | } 504 | 505 | func (cp *meowParent) getServer() string { 506 | return cp.server 507 | } 508 | 509 | func (cp *meowParent) genConfig() string { 510 | method := cp.method 511 | if method == "" { 512 | method = "table" 513 | } 514 | return fmt.Sprintf("proxy = meow://%s:%s@%s", method, cp.passwd, cp.server) 515 | } 516 | 517 | func (cp *meowParent) connect(url *URL) (net.Conn, error) { 518 | c, err := net.Dial("tcp", cp.server) 519 | if err != nil { 520 | errl.Printf("can't connect to meow parent %s for %s: %v\n", 521 | cp.server, url.HostPort, err) 522 | return nil, err 523 | } 524 | debug.Printf("connected to: %s via meow parent: %s\n", 525 | url.HostPort, cp.server) 526 | ssconn := ss.NewConn(c, cp.cipher.Copy()) 527 | return meowConn{ssconn, cp}, nil 528 | } 529 | 530 | // For socks documentation, refer to rfc 1928 http://www.ietf.org/rfc/rfc1928.txt 531 | 532 | var socksError = [...]string{ 533 | 1: "General SOCKS server failure", 534 | 2: "Connection not allowed by ruleset", 535 | 3: "Network unreachable", 536 | 4: "Host unreachable", 537 | 5: "Connection refused", 538 | 6: "TTL expired", 539 | 7: "Command not supported", 540 | 8: "Address type not supported", 541 | 9: "to X'FF' unassigned", 542 | } 543 | 544 | var socksProtocolErr = errors.New("socks protocol error") 545 | 546 | var socksMsgVerMethodSelection = []byte{ 547 | 0x5, // version 5 548 | 1, // n method 549 | 0, // no authorization required 550 | } 551 | 552 | // socks5 parent proxy 553 | type socksParent struct { 554 | server string 555 | } 556 | 557 | type socksConn struct { 558 | net.Conn 559 | parent *socksParent 560 | } 561 | 562 | func (s socksConn) String() string { 563 | return "socks proxy " + s.parent.server 564 | } 565 | 566 | func newSocksParent(server string) *socksParent { 567 | return &socksParent{server} 568 | } 569 | 570 | func (sp *socksParent) getServer() string { 571 | return sp.server 572 | } 573 | 574 | func (sp *socksParent) genConfig() string { 575 | return fmt.Sprintf("proxy = socks5://%s", sp.server) 576 | } 577 | 578 | func (sp *socksParent) connect(url *URL) (net.Conn, error) { 579 | c, err := net.Dial("tcp", sp.server) 580 | if err != nil { 581 | errl.Printf("can't connect to socks parent %s for %s: %v\n", 582 | sp.server, url.HostPort, err) 583 | return nil, err 584 | } 585 | hasErr := false 586 | defer func() { 587 | if hasErr { 588 | c.Close() 589 | } 590 | }() 591 | 592 | var n int 593 | if n, err = c.Write(socksMsgVerMethodSelection); n != 3 || err != nil { 594 | errl.Printf("sending ver/method selection msg %v n = %v\n", err, n) 595 | hasErr = true 596 | return nil, err 597 | } 598 | 599 | // version/method selection 600 | repBuf := make([]byte, 2) 601 | _, err = io.ReadFull(c, repBuf) 602 | if err != nil { 603 | errl.Printf("read ver/method selection error %v\n", err) 604 | hasErr = true 605 | return nil, err 606 | } 607 | if repBuf[0] != 5 || repBuf[1] != 0 { 608 | errl.Printf("socks ver/method selection reply error ver %d method %d", 609 | repBuf[0], repBuf[1]) 610 | hasErr = true 611 | return nil, err 612 | } 613 | // debug.Println("Socks version selection done") 614 | 615 | // send connect request 616 | host := url.Host 617 | port, err := strconv.Atoi(url.Port) 618 | if err != nil { 619 | errl.Printf("should not happen, port error %v\n", port) 620 | hasErr = true 621 | return nil, err 622 | } 623 | 624 | hostLen := len(host) 625 | bufLen := 5 + hostLen + 2 // last 2 is port 626 | reqBuf := make([]byte, bufLen) 627 | reqBuf[0] = 5 // version 5 628 | reqBuf[1] = 1 // cmd: connect 629 | // reqBuf[2] = 0 // rsv: set to 0 when initializing 630 | reqBuf[3] = 3 // atyp: domain name 631 | reqBuf[4] = byte(hostLen) 632 | copy(reqBuf[5:], host) 633 | binary.BigEndian.PutUint16(reqBuf[5+hostLen:5+hostLen+2], uint16(port)) 634 | 635 | if n, err = c.Write(reqBuf); err != nil || n != bufLen { 636 | errl.Printf("send socks request err %v n %d\n", err, n) 637 | hasErr = true 638 | return nil, err 639 | } 640 | 641 | // I'm not clear why the buffer is fixed at 10. The rfc document does not say this. 642 | // Polipo set this to 10 and I also observed the reply is always 10. 643 | replyBuf := make([]byte, 10) 644 | if n, err = c.Read(replyBuf); err != nil { 645 | // Seems that socks server will close connection if it can't find host 646 | if err != io.EOF { 647 | errl.Printf("read socks reply err %v n %d\n", err, n) 648 | } 649 | hasErr = true 650 | return nil, errors.New("connection failed (by socks server " + sp.server + "). No such host?") 651 | } 652 | // debug.Printf("Socks reply length %d\n", n) 653 | 654 | if replyBuf[0] != 5 { 655 | errl.Printf("socks reply connect %s VER %d not supported\n", url.HostPort, replyBuf[0]) 656 | hasErr = true 657 | return nil, socksProtocolErr 658 | } 659 | if replyBuf[1] != 0 { 660 | errl.Printf("socks reply connect %s error %s\n", url.HostPort, socksError[replyBuf[1]]) 661 | hasErr = true 662 | return nil, socksProtocolErr 663 | } 664 | if replyBuf[3] != 1 { 665 | errl.Printf("socks reply connect %s ATYP %d\n", url.HostPort, replyBuf[3]) 666 | hasErr = true 667 | return nil, socksProtocolErr 668 | } 669 | 670 | debug.Println("connected to:", url.HostPort, "via socks server:", sp.server) 671 | // Now the socket can be used to pass data. 672 | return socksConn{c, sp}, nil 673 | } 674 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/cyfdecyf/bufio" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestSendBodyChunked(t *testing.T) { 11 | testData := []struct { 12 | raw string 13 | want string // empty means same as raw 14 | }{ 15 | {"1a; ignore-stuff-here\r\nabcdefghijklmnopqrstuvwxyz\r\n10\r\n1234567890abcdef\r\n0\r\n\r\n", ""}, 16 | {"0\r\n\r\n", ""}, 17 | /* 18 | {"0\n\r\n", "0\r\n\r\n"}, // test for buggy web servers 19 | {"1a; ignore-stuff-here\nabcdefghijklmnopqrstuvwxyz\r\n10\n1234567890abcdef\n0\n\n", 20 | // meow will only sanitize CRLF at chunk ending 21 | "1a; ignore-stuff-here\nabcdefghijklmnopqrstuvwxyz\r\n10\n1234567890abcdef\r\n0\r\n\r\n"}, 22 | */ 23 | } 24 | 25 | // supress error log when finding chunk extension 26 | errl = false 27 | defer func() { 28 | errl = true 29 | }() 30 | // use different reader buffer size to test for both all buffered and partially buffered chunk 31 | sizeArr := []int{32, 64, 128} 32 | for _, size := range sizeArr { 33 | for _, td := range testData { 34 | r := bufio.NewReaderSize(strings.NewReader(td.raw), size) 35 | w := new(bytes.Buffer) 36 | 37 | if err := sendBodyChunked(w, r, size); err != nil { 38 | t.Fatalf("sent data %q err: %v\n", w.Bytes(), err) 39 | } 40 | if td.want == "" { 41 | if w.String() != td.raw { 42 | t.Errorf("sendBodyChunked wrong with buf size %d, raw data is:\n%q\ngot:\n%q\n", 43 | size, td.raw, w.String()) 44 | } 45 | } else { 46 | if w.String() != td.want { 47 | t.Errorf("sendBodyChunked wrong with buf sizwe %d, raw data is:\n%q\nwant:\n%q\ngot :\n%q\n", 48 | size, td.raw, td.want, w.String()) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | func TestInitSelfListenAddr(t *testing.T) { 56 | listenProxy = []Proxy{newHttpProxy("0.0.0.0:4411", "", "http")} 57 | initSelfListenAddr() 58 | 59 | testData := []struct { 60 | r Request 61 | self bool 62 | }{ 63 | {Request{Header: Header{Host: "google.com:443"}, URL: &URL{}}, false}, 64 | {Request{Header: Header{Host: "localhost"}, URL: &URL{}}, true}, 65 | {Request{Header: Header{Host: "127.0.0.1:4411"}, URL: &URL{}}, true}, 66 | {Request{Header: Header{Host: ""}, URL: &URL{HostPort: "google.com"}}, false}, 67 | {Request{Header: Header{Host: "localhost"}, URL: &URL{HostPort: "google.com"}}, false}, 68 | } 69 | 70 | for _, td := range testData { 71 | if isSelfRequest(&td.r) != td.self { 72 | t.Error(td.r.Host, "isSelfRequest should be", td.self) 73 | } 74 | if td.self && td.r.URL.Host == "" { 75 | t.Error("isSelfRequest should set url host", td.r.Header.Host) 76 | } 77 | } 78 | 79 | // Another set of listen addr. 80 | listenProxy = []Proxy{ 81 | newHttpProxy("192.168.1.1:4411", "", "http"), 82 | newHttpProxy("127.0.0.1:8888", "", "http"), 83 | } 84 | initSelfListenAddr() 85 | 86 | testData2 := []struct { 87 | r Request 88 | self bool 89 | }{ 90 | {Request{Header: Header{Host: "google.com:443"}, URL: &URL{}}, false}, 91 | {Request{Header: Header{Host: "localhost"}, URL: &URL{}}, true}, 92 | {Request{Header: Header{Host: "127.0.0.1:8888"}, URL: &URL{}}, true}, 93 | {Request{Header: Header{Host: "192.168.1.1"}, URL: &URL{}}, true}, 94 | {Request{Header: Header{Host: "192.168.1.2"}, URL: &URL{}}, false}, 95 | {Request{Header: Header{Host: ""}, URL: &URL{HostPort: "google.com"}}, false}, 96 | {Request{Header: Header{Host: "localhost"}, URL: &URL{HostPort: "google.com"}}, false}, 97 | } 98 | 99 | for _, td := range testData2 { 100 | if isSelfRequest(&td.r) != td.self { 101 | t.Error(td.r.Host, "isSelfRequest should be", td.self) 102 | } 103 | if td.self && td.r.URL.Host == "" { 104 | t.Error("isSelfRequest should set url host", td.r.Header.Host) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /proxy_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "net" 8 | "syscall" 9 | ) 10 | 11 | func isErrConnReset(err error) bool { 12 | if ne, ok := err.(*net.OpError); ok { 13 | if se, seok := ne.Err.(*os.SyscallError); seok { 14 | return se.Err == syscall.ECONNRESET 15 | } 16 | } 17 | return false 18 | } 19 | 20 | func isDNSError(err error) bool { 21 | if _, ok := err.(*net.DNSError); ok { 22 | return true 23 | } 24 | return false 25 | } 26 | 27 | func isErrOpWrite(err error) bool { 28 | ne, ok := err.(*net.OpError) 29 | if !ok { 30 | return false 31 | } 32 | return ne.Op == "write" 33 | } 34 | 35 | func isErrOpRead(err error) bool { 36 | ne, ok := err.(*net.OpError) 37 | if !ok { 38 | return false 39 | } 40 | return ne.Op == "read" 41 | } 42 | -------------------------------------------------------------------------------- /proxy_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func isErrConnReset(err error) bool { 11 | if ne, ok := err.(*net.OpError); ok { 12 | if se, seok := ne.Err.(*os.SyscallError); seok { 13 | return se.Err == syscall.WSAECONNRESET || se.Err == syscall.ECONNRESET 14 | } 15 | } 16 | return false 17 | } 18 | 19 | func isDNSError(err error) bool { 20 | errMsg := err.Error() 21 | return strings.Contains(errMsg, "No such host") || 22 | strings.Contains(errMsg, "GetAddrInfoW") || 23 | strings.Contains(errMsg, "dial tcp") 24 | } 25 | 26 | func isErrOpWrite(err error) bool { 27 | ne, ok := err.(*net.OpError) 28 | if !ok { 29 | return false 30 | } 31 | return ne.Op == "WSASend" 32 | } 33 | 34 | func isErrOpRead(err error) bool { 35 | ne, ok := err.(*net.OpError) 36 | if !ok { 37 | return false 38 | } 39 | return ne.Op == "WSARecv" 40 | } 41 | -------------------------------------------------------------------------------- /script/MEOW.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netheril96/MEOW/4dc7beca878340274baadb3ec26a508095a4a928/script/MEOW.ico -------------------------------------------------------------------------------- /script/MEOW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netheril96/MEOW/4dc7beca878340274baadb3ec26a508095a4a928/script/MEOW.png -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | # About meow-taskbar.exe 2 | 3 | Copied `goagent.exe`, modified the string table and icon using reshack. 4 | 5 | Thanks for the taskbar project created by @phuslu. 6 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 6 | 7 | version=`grep '^version=' ./install.sh | sed -s 's/version=//'` 8 | go_version=`go version` 9 | echo "creating MEOW binary version $version" 10 | 11 | rm -rf bin 12 | mkdir -p bin/windows 13 | 14 | gox -output="bin/{{.Dir}}-{{.OS}}-{{.Arch}}-$version" -os="windows" 15 | gox -output="bin/{{.Dir}}-{{.OS}}-{{.Arch}}-$version" -osarch="darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm" 16 | 17 | pack() { 18 | local goos 19 | local goarch 20 | local name 21 | 22 | goos=$1 23 | goarch=$2 24 | name=MEOW-$goos-$goarch-$version 25 | 26 | echo "packing $goos $goarch" 27 | if [[ $1 == "windows" ]]; then 28 | mv bin/$name.exe script/proxy.exe 29 | pushd script 30 | sed -e 's/$/\r/' ../doc/sample-config/rc > rc.txt 31 | sed -e 's/$/\r/' ../doc/sample-config/rc-full > rc-full.txt 32 | sed -e 's/$/\r/' ../doc/sample-config/direct > direct.txt 33 | sed -e 's/$/\r/' ../doc/sample-config/proxy > proxy.txt 34 | sed -e 's/$/\r/' ../doc/sample-config/reject > reject.txt 35 | mv meow-taskbar.exe MEOW.exe 36 | zip $name.zip proxy.exe MEOW.exe rc.txt rc-full.txt direct.txt proxy.txt reject.txt 37 | rm -f proxy.exe rc.txt rc-full.txt direct.txt proxy.txt reject.txt 38 | mv $name.zip ../bin/ 39 | mv MEOW.exe meow-taskbar.exe 40 | popd 41 | if [[ $2 == "386" ]]; then 42 | mv bin/$name.zip bin/windows/MEOW-x86-$version.zip 43 | fi 44 | if [[ $2 == "amd64" ]]; then 45 | mv bin/$name.zip bin/windows/MEOW-x64-$version.zip 46 | fi 47 | else 48 | gzip -f bin/$name 49 | fi 50 | } 51 | 52 | pack darwin amd64 53 | pack darwin 386 54 | pack linux amd64 55 | pack linux 386 56 | pack linux arm 57 | pack windows amd64 58 | pack windows 386 59 | 60 | git config --global user.name "renzhn" 61 | git config --global user.email "renzhen999@gmail.com" 62 | 63 | git checkout gh-pages 64 | rm -rf dist 65 | mv bin dist 66 | git add dist 67 | git commit -am"version $version $go_version" 68 | git push -f 69 | git checkout master 70 | -------------------------------------------------------------------------------- /script/debugrc: -------------------------------------------------------------------------------- 1 | listen = meow://aes-128-cfb:foobar@127.0.0.1:8899 2 | -------------------------------------------------------------------------------- /script/httprc: -------------------------------------------------------------------------------- 1 | listen = http://127.0.0.1:7788 2 | proxy = meow://aes-128-cfb:foobar@127.0.0.1:8899 3 | -------------------------------------------------------------------------------- /script/log-group-by-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $# != 1 ]]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | log=$1 9 | 10 | #clients=`egrep 'cli\([^)]+\) connected, total' $log | cut -d ' ' -f 4` 11 | 12 | #for c in $clients; do 13 | #echo $c 14 | #done 15 | 16 | sort --stable --key 4,4 --key 3,3 $log | sed -e "/closed, total/s,\$,\n\n," > $log-grouped 17 | 18 | -------------------------------------------------------------------------------- /script/meow: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### BEGIN INIT INFO 3 | # Provides: MEOW 4 | # Required-Start: $network 5 | # Required-Stop: $network 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: MEOW Proxy 9 | # Description: Automatically detect blocked site and use parent proxy. 10 | ### END INIT INFO 11 | 12 | # Put this script under /etc/init.d/, then run "update-rc.d meow defaults". 13 | 14 | # Note: this script requires sudo in order to run MEOW as the specified 15 | # user. Please change the following variables in order to use this script. 16 | # MEOW will search for rc/direct/block/stat file under user's $HOME/.meow/ directory. 17 | BIN=/usr/local/bin/MEOW 18 | USER=sym 19 | GROUP=sym 20 | PID_DIR=/var/run 21 | PID_FILE=$PID_DIR/meow.pid 22 | LOG_FILE=/var/log/meow 23 | 24 | RET_VAL=0 25 | 26 | check_running() { 27 | if [[ -r $PID_FILE ]]; then 28 | read PID <$PID_FILE 29 | if [[ -d "/proc/$PID" ]]; then 30 | return 0 31 | else 32 | rm -f $PID_FILE 33 | return 1 34 | fi 35 | else 36 | return 2 37 | fi 38 | } 39 | 40 | do_status() { 41 | check_running 42 | case $? in 43 | 0) 44 | echo "meow running with PID $PID" 45 | ;; 46 | 1) 47 | echo "meow not running, remove PID file $PID_FILE" 48 | ;; 49 | 2) 50 | echo "Could not find PID file $PID_FILE, meow does not appear to be running" 51 | ;; 52 | esac 53 | return 0 54 | } 55 | 56 | do_start() { 57 | if [[ ! -d $PID_DIR ]]; then 58 | echo "creating PID dir" 59 | mkdir $PID_DIR || echo "failed creating PID directory $PID_DIR"; exit 1 60 | chown $USER:$GROUP $PID_DIR || echo "failed creating PID directory $PID_DIR"; exit 1 61 | chmod 0770 $PID_DIR 62 | fi 63 | if check_running; then 64 | echo "meow already running with PID $PID" 65 | return 0 66 | fi 67 | echo "starting meow" 68 | # sudo will set the group to the primary group of $USER 69 | sudo -u $USER -H -- $BIN >$LOG_FILE 2>&1 & 70 | PID=$! 71 | echo $PID > $PID_FILE 72 | sleep 0.3 73 | if ! check_running; then 74 | echo "start failed" 75 | return 1 76 | fi 77 | echo "meow running with PID $PID" 78 | return 0 79 | } 80 | 81 | do_stop() { 82 | if check_running; then 83 | echo "stopping meow with PID $PID" 84 | kill $PID 85 | rm -f $PID_FILE 86 | else 87 | echo "Could not find PID file $PID_FILE" 88 | fi 89 | } 90 | 91 | do_restart() { 92 | do_stop 93 | do_start 94 | } 95 | 96 | case "$1" in 97 | start|stop|restart|status) 98 | do_$1 99 | ;; 100 | *) 101 | echo "Usage: meow {start|stop|restart|status}" 102 | RET_VAL=1 103 | ;; 104 | esac 105 | 106 | exit $RET_VAL 107 | -------------------------------------------------------------------------------- /script/meow-taskbar.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netheril96/MEOW/4dc7beca878340274baadb3ec26a508095a4a928/script/meow-taskbar.exe -------------------------------------------------------------------------------- /script/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 4 | 5 | if [ $# != 1 ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | version=$1 11 | #echo $version 12 | 13 | sed -i -e "s,\(\tversion \+= \)\".*\"$,\1\"$version\"," config.go 14 | sed -i -e "s/version=.*$/version=$version/" install.sh 15 | sed -i -e "s/当前版本:[^ ]\+ \(.*\)\$/当前版本:$version \1/" README.md 16 | 17 | -------------------------------------------------------------------------------- /script/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 4 | 5 | if ! go build; then 6 | echo "build failed" 7 | exit 1 8 | fi 9 | 10 | PROXY_ADDR=127.0.0.1:7788 11 | MEOW_ADDR=127.0.0.1:8899 12 | 13 | if [[ -z "$TRAVIS" ]]; then 14 | RCDIR=~/.meow/ 15 | else # on travis 16 | RCDIR=./script/ 17 | fi 18 | 19 | ./MEOW -request=false -reply=false -rc $RCDIR/debugrc -listen=meow://aes-128-cfb:foobar@$MEOW_ADDR & 20 | parent_pid=$! 21 | ./MEOW -request=false -reply=false -rc ./script/httprc -listen=http://$PROXY_ADDR & 22 | meow_pid=$! 23 | 24 | stop_meow() { 25 | kill -SIGTERM $parent_pid 26 | kill -SIGTERM $meow_pid 27 | } 28 | trap 'stop_meow' TERM INT 29 | 30 | sleep 1 31 | 32 | test_get() { 33 | local url 34 | url=$1 35 | target=$2 36 | noproxy=$3 37 | code=$4 38 | 39 | echo -n "GET $url " 40 | if [[ -z $code ]]; then 41 | code="200" 42 | fi 43 | 44 | # -s silent to disable progress meter, but enable --show-error 45 | # -i to include http header 46 | # -L to follow redirect so we should always get HTTP 200 47 | if [[ -n $noproxy ]]; then 48 | cont=`curl -s --show-error -i -L $url 2>&1` 49 | else 50 | cont=`curl -s --show-error -i -L -x $PROXY_ADDR $url 2>&1` 51 | fi 52 | ok=`echo $cont | grep -E -o "HTTP/1\.1 +$code"` 53 | html=`echo $cont | grep -E -o -i "$target"` 54 | if [[ -z $ok || -z $html ]] ; then 55 | echo "==============================" 56 | echo "GET $url FAILED!!!" 57 | echo "$ok" 58 | echo "$html" 59 | echo $cont 60 | echo "==============================" 61 | kill -SIGTERM $meow_pid 62 | exit 1 63 | fi 64 | sleep 0.3 65 | echo "passed" 66 | } 67 | 68 | test_get $PROXY_ADDR/pac "proxy-autoconfig" "noproxy" # test for pac 69 | test_get google.com "" # blocked site, all kinds of block method 73 | test_get https://google.com "" 76 | 77 | # Sites that may timeout on travis. 78 | if [[ -z $TRAVIS ]]; then 79 | test_get plan9.bell-labs.com/magic/man2html/1/2l "" "" "404" # single LF in response header 80 | test_get www.wpxap.com "" # 301 redirect 83 | test_get www.taobao.com "" # chunked encoding, weird can't tests for in script 84 | test_get https://www.alipay.com "" 85 | fi 86 | 87 | stop_meow 88 | exit 0 89 | -------------------------------------------------------------------------------- /script/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 4 | 5 | if [[ $# != 2 ]]; then 6 | echo "upload.sh " 7 | exit 1 8 | fi 9 | 10 | version=`grep '^version=' ./install-meow.sh | sed -s 's/version=//'` 11 | username=$1 12 | passwd=$2 13 | 14 | upload() { 15 | summary=$1 16 | file=$2 17 | googlecode_upload.py -l Featured -u "$username" -w "$passwd" -s "$summary" -p meow-proxy "$file" 18 | } 19 | 20 | upload "$version for Linux 32bit" bin/meow-linux32-$version.gz 21 | upload "$version for Linux 64bit" bin/meow-linux64-$version.gz 22 | upload "$version for Windows 64bit" bin/meow-win64-$version.zip 23 | upload "$version for Windows 32bit" bin/meow-win32-$version.zip 24 | upload "$version for OS X 64bit" bin/meow-mac64-$version.gz 25 | -------------------------------------------------------------------------------- /ssh.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os/exec" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func SshRunning(socksServer string) bool { 11 | c, err := net.Dial("tcp", socksServer) 12 | if err != nil { 13 | return false 14 | } 15 | c.Close() 16 | return true 17 | } 18 | 19 | func runOneSSH(server string) { 20 | // config parsing canonicalize sshServer config value 21 | arr := strings.SplitN(server, ":", 3) 22 | sshServer, localPort, sshPort := arr[0], arr[1], arr[2] 23 | alreadyRunPrinted := false 24 | 25 | socksServer := "127.0.0.1:" + localPort 26 | for { 27 | if SshRunning(socksServer) { 28 | if !alreadyRunPrinted { 29 | debug.Println("ssh socks server", socksServer, "maybe already running") 30 | alreadyRunPrinted = true 31 | } 32 | time.Sleep(30 * time.Second) 33 | continue 34 | } 35 | 36 | // -n redirects stdin from /dev/null 37 | // -N do not execute remote command 38 | debug.Println("connecting to ssh server", sshServer+":"+sshPort) 39 | cmd := exec.Command("ssh", "-n", "-N", "-D", localPort, "-p", sshPort, sshServer) 40 | if err := cmd.Run(); err != nil { 41 | debug.Println("ssh:", err) 42 | } 43 | debug.Println("ssh", sshServer+":"+sshPort, "exited, reconnect") 44 | time.Sleep(5 * time.Second) 45 | alreadyRunPrinted = false 46 | } 47 | } 48 | 49 | func runSSH() { 50 | for _, server := range config.SshServer { 51 | go runOneSSH(server) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | // Proxy statistics. 2 | 3 | package main 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | var status struct { 11 | cliCnt int32 // number of client connections 12 | srvConnCnt map[string]int // number of connections for each host:port 13 | srvConnCntMutex sync.Mutex 14 | } 15 | 16 | func initStat() { 17 | if !debug { 18 | return 19 | } 20 | status.srvConnCnt = make(map[string]int) 21 | } 22 | 23 | func incCliCnt() int32 { 24 | atomic.AddInt32(&status.cliCnt, 1) 25 | return status.cliCnt 26 | } 27 | 28 | func decCliCnt() int32 { 29 | atomic.AddInt32(&status.cliCnt, -1) 30 | return status.cliCnt 31 | } 32 | 33 | func addSrvConnCnt(srv string, delta int) int { 34 | status.srvConnCntMutex.Lock() 35 | status.srvConnCnt[srv] += delta 36 | cnt := status.srvConnCnt[srv] 37 | status.srvConnCntMutex.Unlock() 38 | return int(cnt) 39 | } 40 | 41 | func incSrvConnCnt(srv string) int { 42 | return addSrvConnCnt(srv, 1) 43 | } 44 | 45 | func decSrvConnCnt(srv string) int { 46 | return addSrvConnCnt(srv, -1) 47 | } 48 | -------------------------------------------------------------------------------- /testdata/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netheril96/MEOW/4dc7beca878340274baadb3ec26a508095a4a928/testdata/file -------------------------------------------------------------------------------- /timeoutset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type TimeoutSet struct { 9 | sync.RWMutex 10 | time map[string]time.Time 11 | timeout time.Duration 12 | } 13 | 14 | func NewTimeoutSet(timeout time.Duration) *TimeoutSet { 15 | ts := &TimeoutSet{time: make(map[string]time.Time), 16 | timeout: timeout, 17 | } 18 | return ts 19 | } 20 | 21 | func (ts *TimeoutSet) add(key string) { 22 | now := time.Now() 23 | ts.Lock() 24 | ts.time[key] = now 25 | ts.Unlock() 26 | } 27 | 28 | func (ts *TimeoutSet) has(key string) bool { 29 | ts.RLock() 30 | t, ok := ts.time[key] 31 | ts.RUnlock() 32 | if !ok { 33 | return false 34 | } 35 | if time.Now().Sub(t) > ts.timeout { 36 | ts.del(key) 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | func (ts *TimeoutSet) del(key string) { 43 | ts.Lock() 44 | delete(ts.time, key) 45 | ts.Unlock() 46 | } 47 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "os" 12 | "path" 13 | "runtime" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/cyfdecyf/bufio" 18 | ) 19 | 20 | const isWindows = runtime.GOOS == "windows" 21 | 22 | type notification chan byte 23 | 24 | func newNotification() notification { 25 | // Notification channel has size 1, so sending a single one will not block 26 | return make(chan byte, 1) 27 | } 28 | 29 | func (n notification) notify() { 30 | n <- 1 31 | } 32 | 33 | func (n notification) hasNotified() bool { 34 | select { 35 | case <-n: 36 | return true 37 | default: 38 | return false 39 | } 40 | } 41 | 42 | func ASCIIToUpperInplace(b []byte) { 43 | for i := 0; i < len(b); i++ { 44 | if 97 <= b[i] && b[i] <= 122 { 45 | b[i] -= 32 46 | } 47 | } 48 | } 49 | 50 | func ASCIIToUpper(b []byte) []byte { 51 | buf := make([]byte, len(b)) 52 | for i := 0; i < len(b); i++ { 53 | if 97 <= b[i] && b[i] <= 122 { 54 | buf[i] = b[i] - 32 55 | } else { 56 | buf[i] = b[i] 57 | } 58 | } 59 | return buf 60 | } 61 | 62 | func ASCIIToLowerInplace(b []byte) { 63 | for i := 0; i < len(b); i++ { 64 | if 65 <= b[i] && b[i] <= 90 { 65 | b[i] += 32 66 | } 67 | } 68 | } 69 | 70 | func ASCIIToLower(b []byte) []byte { 71 | buf := make([]byte, len(b)) 72 | for i := 0; i < len(b); i++ { 73 | if 65 <= b[i] && b[i] <= 90 { 74 | buf[i] = b[i] + 32 75 | } else { 76 | buf[i] = b[i] 77 | } 78 | } 79 | return buf 80 | } 81 | 82 | func IsDigit(b byte) bool { 83 | return '0' <= b && b <= '9' 84 | } 85 | 86 | var spaceTbl = [256]bool{ 87 | '\t': true, // ht 88 | '\n': true, // lf 89 | '\r': true, // cr 90 | ' ': true, // sp 91 | } 92 | 93 | func IsSpace(b byte) bool { 94 | return spaceTbl[b] 95 | } 96 | 97 | func TrimSpace(s []byte) []byte { 98 | st := 0 99 | end := len(s) - 1 100 | for ; st < len(s) && IsSpace(s[st]); st++ { 101 | } 102 | if st == len(s) { 103 | return s[:0] 104 | } 105 | for ; end >= 0 && IsSpace(s[end]); end-- { 106 | } 107 | return s[st : end+1] 108 | } 109 | 110 | func TrimTrailingSpace(s []byte) []byte { 111 | end := len(s) - 1 112 | for ; end >= 0 && IsSpace(s[end]); end-- { 113 | } 114 | return s[:end+1] 115 | } 116 | 117 | // FieldsN is simliar with bytes.Fields, but only consider space and '\t' as 118 | // space, and will include all content in the final slice with ending white 119 | // space characters trimmed. bytes.Split can't split on both space and '\t', 120 | // and considers two separator as an empty item. bytes.FieldsFunc can't 121 | // specify how much fields we need, which is required for parsing response 122 | // status line. Returns nil if n < 0. 123 | func FieldsN(s []byte, n int) [][]byte { 124 | if n <= 0 { 125 | return nil 126 | } 127 | res := make([][]byte, n) 128 | na := 0 129 | fieldStart := -1 130 | var i int 131 | for ; i < len(s); i++ { 132 | issep := s[i] == ' ' || s[i] == '\t' 133 | if fieldStart < 0 && !issep { 134 | fieldStart = i 135 | } 136 | if fieldStart >= 0 && issep { 137 | if na == n-1 { 138 | break 139 | } 140 | res[na] = s[fieldStart:i] 141 | na++ 142 | fieldStart = -1 143 | } 144 | } 145 | if fieldStart >= 0 { // must have na <= n-1 here 146 | res[na] = TrimSpace(s[fieldStart:]) 147 | if len(res[na]) != 0 { // do not consider ending space as a field 148 | na++ 149 | } 150 | } 151 | return res[:na] 152 | } 153 | 154 | var digitTbl = [256]int8{ 155 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 156 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 157 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 158 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, 159 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, 160 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 161 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, 162 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 163 | } 164 | 165 | // ParseIntFromBytes parse hexidecimal number from given bytes. 166 | // No prefix (e.g. 0xdeadbeef) should given. 167 | // base can only be 10 or 16. 168 | func ParseIntFromBytes(b []byte, base int) (n int64, err error) { 169 | // Currently, we have to convert []byte to string to use strconv 170 | // Refer to: http://code.google.com/p/go/issues/detail?id=2632 171 | // That's why I created this function. 172 | if base != 10 && base != 16 { 173 | err = errors.New(fmt.Sprintf("invalid base: %d", base)) 174 | return 175 | } 176 | if len(b) == 0 { 177 | err = errors.New("parse int from empty bytes") 178 | return 179 | } 180 | 181 | neg := false 182 | if b[0] == '+' { 183 | b = b[1:] 184 | } else if b[0] == '-' { 185 | b = b[1:] 186 | neg = true 187 | } 188 | 189 | for _, d := range b { 190 | v := digitTbl[d] 191 | if v == -1 { 192 | n = 0 193 | err = errors.New(fmt.Sprintf("invalid number: %s", b)) 194 | return 195 | } 196 | if int(v) >= base { 197 | n = 0 198 | err = errors.New(fmt.Sprintf("invalid base %d number: %s", base, b)) 199 | return 200 | } 201 | n *= int64(base) 202 | n += int64(v) 203 | } 204 | if neg { 205 | n = -n 206 | } 207 | return 208 | } 209 | 210 | func isFileExists(path string) error { 211 | stat, err := os.Stat(path) 212 | if err != nil { 213 | return err 214 | } 215 | if !stat.Mode().IsRegular() { 216 | return fmt.Errorf("%s is not regular file", path) 217 | } 218 | return nil 219 | } 220 | 221 | func isDirExists(path string) error { 222 | stat, err := os.Stat(path) 223 | if err != nil { 224 | return err 225 | } 226 | if !stat.IsDir() { 227 | return fmt.Errorf("%s is not directory", path) 228 | } 229 | return nil 230 | } 231 | 232 | func getUserHomeDir() string { 233 | home := os.Getenv("HOME") 234 | if home == "" { 235 | fmt.Println("HOME environment variable is empty") 236 | } 237 | return home 238 | } 239 | 240 | func expandTilde(pth string) string { 241 | if len(pth) > 0 && pth[0] == '~' { 242 | home := getUserHomeDir() 243 | return path.Join(home, pth[1:]) 244 | } 245 | return pth 246 | } 247 | 248 | // copyN copys N bytes from src to dst, reading at most rdSize for each read. 249 | // rdSize should <= buffer size of the buffered reader. 250 | // Returns any encountered error. 251 | func copyN(dst io.Writer, src *bufio.Reader, n, rdSize int) (err error) { 252 | // Most of the copy is copied from io.Copy 253 | for n > 0 { 254 | var b []byte 255 | var er error 256 | if n > rdSize { 257 | b, er = src.ReadN(rdSize) 258 | } else { 259 | b, er = src.ReadN(n) 260 | } 261 | nr := len(b) 262 | n -= nr 263 | if nr > 0 { 264 | nw, ew := dst.Write(b) 265 | if ew != nil { 266 | err = ew 267 | break 268 | } 269 | if nr != nw { 270 | err = io.ErrShortWrite 271 | break 272 | } 273 | } 274 | if er == io.EOF { 275 | break 276 | } 277 | if er != nil { 278 | err = er 279 | break 280 | } 281 | } 282 | return err 283 | } 284 | 285 | func md5sum(ss ...string) string { 286 | h := md5.New() 287 | for _, s := range ss { 288 | io.WriteString(h, s) 289 | } 290 | return fmt.Sprintf("%x", h.Sum(nil)) 291 | } 292 | 293 | // hostIsIP determines whether a host address is an IP address and whether 294 | // it is private. Currenly only handles IPv4 addresses. 295 | func hostIsIP(host string) (isIP, isPrivate bool) { 296 | part := strings.Split(host, ".") 297 | if len(part) != 4 { 298 | return false, false 299 | } 300 | for _, i := range part { 301 | if len(i) == 0 || len(i) > 3 { 302 | return false, false 303 | } 304 | n, err := strconv.Atoi(i) 305 | if err != nil || n < 0 || n > 255 { 306 | return false, false 307 | } 308 | } 309 | if part[0] == "127" || part[0] == "10" || (part[0] == "192" && part[1] == "168") { 310 | return true, true 311 | } 312 | if part[0] == "172" { 313 | n, _ := strconv.Atoi(part[1]) 314 | if 16 <= n && n <= 31 { 315 | return true, true 316 | } 317 | } 318 | return true, false 319 | } 320 | 321 | // NetNbitIPv4Mask returns a IPMask with highest n bit set. 322 | func NewNbitIPv4Mask(n int) net.IPMask { 323 | if n > 32 { 324 | panic("NewNbitIPv4Mask: bit number > 32") 325 | } 326 | mask := []byte{0, 0, 0, 0} 327 | for id := 0; id < 4; id++ { 328 | if n >= 8 { 329 | mask[id] = 0xff 330 | } else { 331 | mask[id] = ^byte(1<<(uint8(8-n)) - 1) 332 | break 333 | } 334 | n -= 8 335 | } 336 | return net.IPMask(mask) 337 | } 338 | 339 | func trimLastDot(s string) string { 340 | if len(s) > 0 && s[len(s)-1] == '.' { 341 | return s[:len(s)-1] 342 | } 343 | return s 344 | } 345 | 346 | // host2Domain returns the domain of a host. It will recognize domains like 347 | // google.com.hk. Returns empty string for simple host and internal IP. 348 | func host2Domain(host string) (domain string) { 349 | isIP, isPrivate := hostIsIP(host) 350 | if isPrivate { 351 | return "" 352 | } 353 | if isIP { 354 | return host 355 | } 356 | host = trimLastDot(host) 357 | lastDot := strings.LastIndex(host, ".") 358 | if lastDot == -1 { 359 | return "" 360 | } 361 | // Find the 2nd last dot 362 | dot2ndLast := strings.LastIndex(host[:lastDot], ".") 363 | if dot2ndLast == -1 { 364 | return host 365 | } 366 | 367 | return host[dot2ndLast+1:] 368 | } 369 | 370 | // IgnoreUTF8BOM consumes UTF-8 encoded BOM character if present in the file. 371 | func IgnoreUTF8BOM(f *os.File) error { 372 | bom := make([]byte, 3) 373 | n, err := f.Read(bom) 374 | if err != nil { 375 | return err 376 | } 377 | if n != 3 { 378 | return nil 379 | } 380 | if bytes.Equal(bom, []byte{0xEF, 0xBB, 0xBF}) { 381 | debug.Println("UTF-8 BOM found") 382 | return nil 383 | } 384 | // No BOM found, seek back 385 | _, err = f.Seek(-3, 1) 386 | return err 387 | } 388 | 389 | // Return all host IP addresses. 390 | func hostAddr() (addr []string) { 391 | allAddr, err := net.InterfaceAddrs() 392 | if err != nil { 393 | Fatal("error getting host address", err) 394 | } 395 | for _, ad := range allAddr { 396 | ads := ad.String() 397 | id := strings.Index(ads, "/") 398 | if id == -1 { 399 | // On windows, no network mask. 400 | id = len(ads) 401 | } 402 | addr = append(addr, ads[:id]) 403 | } 404 | return addr 405 | } 406 | 407 | // ip to long int 408 | func ip2long(ipstr string) (uint32, error) { 409 | ip := net.ParseIP(ipstr) 410 | if ip == nil { 411 | return 0, errors.New("Invalid IP") 412 | } 413 | ip = ip.To4() 414 | if ip == nil { 415 | return 0, errors.New("Not IPv4") 416 | } 417 | return binary.BigEndian.Uint32(ip), nil 418 | } 419 | 420 | // search between [start, end] 421 | func searchRange(start, end int, f func(int) bool) int { 422 | i, j := start, end+1 423 | for i < j { 424 | h := i + (j-i)/2 // avoid overflow when computing h 425 | // i ≤ h < j 426 | if !f(h) { 427 | i = h + 1 // preserves f(i-1) == false 428 | } else { 429 | j = h // preserves f(j) == true 430 | } 431 | } 432 | return i 433 | } 434 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/cyfdecyf/bufio" 10 | ) 11 | 12 | func TestASCIIToUpper(t *testing.T) { 13 | testData := []struct { 14 | raw []byte 15 | upper []byte 16 | }{ 17 | {[]byte("foobar"), []byte("FOOBAR")}, 18 | {[]byte("fOoBAr"), []byte("FOOBAR")}, 19 | {[]byte("..fOoBAr\n"), []byte("..FOOBAR\n")}, 20 | } 21 | for _, td := range testData { 22 | up := ASCIIToUpper(td.raw) 23 | if !bytes.Equal(up, td.upper) { 24 | t.Errorf("raw: %s, upper: %s\n", td.raw, up) 25 | } 26 | } 27 | } 28 | 29 | func TestASCIIToLower(t *testing.T) { 30 | testData := []struct { 31 | raw []byte 32 | lower []byte 33 | }{ 34 | {[]byte("FOOBAR"), []byte("foobar")}, 35 | {[]byte("fOoBAr"), []byte("foobar")}, 36 | {[]byte("..fOoBAr\n"), []byte("..foobar\n")}, 37 | } 38 | for _, td := range testData { 39 | low := ASCIIToLower(td.raw) 40 | if !bytes.Equal(low, td.lower) { 41 | t.Errorf("raw: %s, lower: %s\n", td.raw, low) 42 | } 43 | } 44 | } 45 | 46 | func TestIsDigit(t *testing.T) { 47 | for i := 0; i < 10; i++ { 48 | digit := '0' + byte(i) 49 | letter := 'a' + byte(i) 50 | 51 | if IsDigit(digit) != true { 52 | t.Errorf("%c should return true", digit) 53 | } 54 | 55 | if IsDigit(letter) == true { 56 | t.Errorf("%c should return false", letter) 57 | } 58 | } 59 | } 60 | 61 | func TestIsSpace(t *testing.T) { 62 | testData := []struct { 63 | c byte 64 | is bool 65 | }{ 66 | {'a', false}, 67 | {'B', false}, 68 | {'z', false}, 69 | {'(', false}, 70 | {'}', false}, 71 | {' ', true}, 72 | {'\r', true}, 73 | {'\t', true}, 74 | {'\n', true}, 75 | } 76 | for _, td := range testData { 77 | if IsSpace(td.c) != td.is { 78 | t.Errorf("%v isspace wrong", rune(td.c)) 79 | } 80 | } 81 | } 82 | 83 | func TestTrimSpace(t *testing.T) { 84 | testData := []struct { 85 | old string 86 | trimed string 87 | }{ 88 | {"hello", "hello"}, 89 | {" hello", "hello"}, 90 | {" hello\r\n ", "hello"}, 91 | {" hello \t ", "hello"}, 92 | {"", ""}, 93 | {"\r\n", ""}, 94 | } 95 | for _, td := range testData { 96 | trimed := string(TrimSpace([]byte(td.old))) 97 | if trimed != td.trimed { 98 | t.Errorf("%s trimmed to %s, wrong", td.old, trimed) 99 | } 100 | } 101 | } 102 | 103 | func TestTrimTrailingSpace(t *testing.T) { 104 | testData := []struct { 105 | old string 106 | trimed string 107 | }{ 108 | {"hello", "hello"}, 109 | {" hello", " hello"}, 110 | {" hello\r\n ", " hello"}, 111 | {" hello \t ", " hello"}, 112 | {"", ""}, 113 | {"\r\n", ""}, 114 | } 115 | for _, td := range testData { 116 | trimed := string(TrimTrailingSpace([]byte(td.old))) 117 | if trimed != td.trimed { 118 | t.Errorf("%s trimmed to %s, should be %s\n", td.old, trimed, td.trimed) 119 | } 120 | } 121 | } 122 | 123 | func TestFieldsN(t *testing.T) { 124 | testData := []struct { 125 | raw string 126 | n int 127 | arr []string 128 | }{ 129 | {"", 2, nil}, // this should not crash 130 | {"hello world", -1, nil}, 131 | {"hello \t world welcome", 1, []string{"hello \t world welcome"}}, 132 | {" hello \t world welcome ", 1, []string{"hello \t world welcome"}}, 133 | {"hello world", 2, []string{"hello", "world"}}, 134 | {" hello\tworld ", 2, []string{"hello", "world"}}, 135 | // note \r\n in the middle of a string will be considered as a field 136 | {" hello world \r\n", 4, []string{"hello", "world"}}, 137 | {" hello \t world welcome\r\n", 2, []string{"hello", "world welcome"}}, 138 | {" hello \t world welcome \t ", 2, []string{"hello", "world welcome"}}, 139 | } 140 | 141 | for _, td := range testData { 142 | arr := FieldsN([]byte(td.raw), td.n) 143 | if len(arr) != len(td.arr) { 144 | t.Fatalf("%q want %d fields, got %d\n", td.raw, len(td.arr), len(arr)) 145 | } 146 | for i := 0; i < len(arr); i++ { 147 | if string(arr[i]) != td.arr[i] { 148 | t.Errorf("%q %d item, want %q, got %q\n", td.raw, i, td.arr[i], arr[i]) 149 | } 150 | } 151 | } 152 | } 153 | 154 | func TestParseIntFromBytes(t *testing.T) { 155 | errDummy := errors.New("dummy error") 156 | testData := []struct { 157 | raw []byte 158 | base int 159 | err error 160 | val int64 161 | }{ 162 | {[]byte("123"), 10, nil, 123}, 163 | {[]byte("+123"), 10, nil, 123}, 164 | {[]byte("-123"), 10, nil, -123}, 165 | {[]byte("0"), 10, nil, 0}, 166 | {[]byte("a"), 10, errDummy, 0}, 167 | {[]byte("aBc"), 16, nil, 0xabc}, 168 | {[]byte("+aBc"), 16, nil, 0xabc}, 169 | {[]byte("-aBc"), 16, nil, -0xabc}, 170 | {[]byte("213e"), 16, nil, 0x213e}, 171 | {[]byte("12deadbeef"), 16, nil, 0x12deadbeef}, 172 | {[]byte("213n"), 16, errDummy, 0}, 173 | } 174 | for _, td := range testData { 175 | val, err := ParseIntFromBytes(td.raw, td.base) 176 | if err != nil && td.err == nil { 177 | t.Errorf("%s base %d should NOT return error: %v\n", td.raw, td.base, err) 178 | } 179 | if err == nil && td.err != nil { 180 | t.Errorf("%s base %d should return error\n", td.raw, td.base) 181 | } 182 | if val != td.val { 183 | t.Errorf("%s base %d got wrong value: %d\n", td.raw, td.base, val) 184 | } 185 | } 186 | } 187 | 188 | func TestCopyN(t *testing.T) { 189 | testStr := "go is really a nice language" 190 | for _, step := range []int{4, 9, 17, 32} { 191 | src := bufio.NewReader(strings.NewReader(testStr)) 192 | dst := new(bytes.Buffer) 193 | 194 | err := copyN(dst, src, len(testStr), step) 195 | if err != nil { 196 | t.Error("unexpected err:", err) 197 | break 198 | } 199 | if dst.String() != testStr { 200 | t.Errorf("step %d want %q, got: %q\n", step, testStr, dst.Bytes()) 201 | } 202 | } 203 | } 204 | 205 | func TestIsFileExists(t *testing.T) { 206 | err := isFileExists("testdata") 207 | if err == nil { 208 | t.Error("should return error is path is directory") 209 | } 210 | 211 | err = isFileExists("testdata/none") 212 | if err == nil { 213 | t.Error("Not existing file should return error") 214 | } 215 | 216 | err = isFileExists("testdata/file") 217 | if err != nil { 218 | t.Error("Why error for existing file?") 219 | } 220 | } 221 | 222 | func TestNewNbitIPv4Mask(t *testing.T) { 223 | mask := []byte(NewNbitIPv4Mask(32)) 224 | for i := 0; i < 4; i++ { 225 | if mask[i] != 0xff { 226 | t.Error("NewNbitIPv4Mask with 32 error") 227 | } 228 | } 229 | mask = []byte(NewNbitIPv4Mask(5)) 230 | if mask[0] != 0xf8 || mask[1] != 0 || mask[2] != 0 { 231 | t.Error("NewNbitIPv4Mask with 5 error:", mask) 232 | } 233 | mask = []byte(NewNbitIPv4Mask(9)) 234 | if mask[0] != 0xff || mask[1] != 0x80 || mask[2] != 0 { 235 | t.Error("NewNbitIPv4Mask with 9 error:", mask) 236 | } 237 | mask = []byte(NewNbitIPv4Mask(23)) 238 | if mask[0] != 0xff || mask[1] != 0xff || mask[2] != 0xfe || mask[3] != 0 { 239 | t.Error("NewNbitIPv4Mask with 23 error:", mask) 240 | } 241 | mask = []byte(NewNbitIPv4Mask(28)) 242 | if mask[0] != 0xff || mask[1] != 0xff || mask[2] != 0xff || mask[3] != 0xf0 { 243 | t.Error("NewNbitIPv4Mask with 28 error:", mask) 244 | } 245 | } 246 | 247 | func TestHost2Domain(t *testing.T) { 248 | var testData = []struct { 249 | host string 250 | domain string 251 | }{ 252 | {"www.google.com", "google.com"}, 253 | {"google.com", "google.com"}, 254 | {"com.cn", "com.cn"}, 255 | {"sina.com.cn", "com.cn"}, 256 | {"www.bbc.co.uk", "co.uk"}, 257 | {"apple.com.cn", "com.cn"}, 258 | {"simplehost", ""}, 259 | {"192.168.1.1", ""}, 260 | {"10.2.1.1", ""}, 261 | {"123.45.67.89", "123.45.67.89"}, 262 | {"172.65.43.21", "172.65.43.21"}, 263 | } 264 | 265 | for _, td := range testData { 266 | dm := host2Domain(td.host) 267 | if dm != td.domain { 268 | t.Errorf("%s got domain %v should be %v", td.host, dm, td.domain) 269 | } 270 | } 271 | } 272 | 273 | func TestHostIsIP(t *testing.T) { 274 | var testData = []struct { 275 | host string 276 | isIP bool 277 | isPri bool 278 | }{ 279 | {"127.0.0.1", true, true}, 280 | {"127.2.1.1", true, true}, 281 | {"192.168.1.1", true, true}, 282 | {"10.2.3.4", true, true}, 283 | {"172.16.5.3", true, true}, 284 | {"172.20.5.3", true, true}, 285 | {"172.31.5.3", true, true}, 286 | {"172.15.1.1", true, false}, 287 | {"123.45.67.89", true, false}, 288 | {"foo.com", false, false}, 289 | {"www.foo.com", false, false}, 290 | {"www.bar.foo.com", false, false}, 291 | } 292 | 293 | for _, td := range testData { 294 | isIP, isPri := hostIsIP(td.host) 295 | if isIP != td.isIP { 296 | if td.isIP { 297 | t.Error(td.host, "is IP address") 298 | } else { 299 | t.Error(td.host, "is NOT IP address") 300 | } 301 | } 302 | if isPri != td.isPri { 303 | if td.isPri { 304 | t.Error(td.host, "is private IP address") 305 | } else { 306 | t.Error(td.host, "is NOT private IP address") 307 | } 308 | } 309 | } 310 | } 311 | --------------------------------------------------------------------------------