├── .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 | [](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 |
--------------------------------------------------------------------------------