├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README-en.md ├── README.md ├── auth.go ├── auth_test.go ├── config.go ├── config_test.go ├── config_unix.go ├── config_windows.go ├── conn_pool.go ├── conn_pool_test.go ├── doc ├── implementation.md ├── init.d │ └── cow ├── logrotate.d │ └── cow ├── osx │ └── info.chenyufei.cow.plist └── sample-config │ ├── rc │ └── rc-en ├── error.go ├── estimate_timeout.go ├── http.go ├── http_test.go ├── install-cow.sh ├── log.go ├── main.go ├── main_unix.go ├── main_windows.go ├── pac.go ├── pac.js ├── parent_proxy.go ├── proxy.go ├── proxy_test.go ├── proxy_unix.go ├── proxy_windows.go ├── script ├── README.md ├── build.sh ├── cow-hide.exe ├── cow-taskbar.exe ├── debugrc ├── httprc ├── log-group-by-client.sh ├── set-version.sh ├── test.sh └── upload.sh ├── site_blocked.go ├── site_direct.go ├── sitestat.go ├── sitestat_test.go ├── ssh.go ├── stat.go ├── testdata └── file ├── timeoutset.go ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime* 2 | cow-proxy 3 | bin 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4.2 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: -------------------------------------------------------------------------------- 1 | 0.9.8 (2016-06-19) 2 | * Fix OTA support bug in shadowsocks (report by @defia) 3 | * Fix WeChat image url problem (by @breath-co2 @haha1903) 4 | * Fix connection reset detection (by @fgid) 5 | 6 | 0.9.7 (2016-05-04) 7 | * Support shadowsocks OTA 8 | 9 | 0.9.6 (2015-06-07) 10 | * Reload config by sending SIGUSR1 on Unix system 11 | * Load blocked/direct/stat file from same directory as rc file by default 12 | * Allow user to specify blocked/direct/stat file path 13 | * Detect arm without vfp in install script. 14 | * Fix estimate timeout bug 15 | 16 | 0.9.5 (2015-05-12) 17 | * Support new encryption method "chacha20" and "salsa20" 18 | * Avoid biased parent proxy selection for hash load balacing 19 | * Fix AirDrop on OS X when using PAC 20 | * Fix failed start with corrupted stat file 21 | * Support changing the estimate timeout target 22 | 23 | 0.9.4 (2014-10-08) 24 | * Bug fix (#179): close stat file after load 25 | 26 | 0.9.3 (2014-09-21) 27 | * Support new encryption method "rc4-md5" 28 | 29 | 0.9.2 (2014-07-23) 30 | * Reduce the possibility of encountering too many open file error 31 | * New connection latency based load balancing 32 | * Fix auto load plist for OS X 33 | * Identify blocked site by HTTP error code 34 | 35 | 0.9.1 (2013-12-20) 36 | * Fix can't save site stat bug 37 | * Improve install and startup script 38 | 39 | 0.9 (2013-12-02) 40 | * New feature: two COW servers can be connected using encrypted 41 | connection, thus we have an encrypted HTTP proxy chain that can 42 | be used to bypass the firewall 43 | * Allow client to use HTTP basic authentication 44 | * Simplify configuration syntax 45 | * Better reuse for HTTP parent connections 46 | * Reduce direct/blocked delta 47 | * Generate new PAC every minute 48 | 49 | 0.8 (2013-08-10) 50 | * Share server connections between different clients 51 | * Add tunnelAllowedPort option to limit ports CONNECT method can connect to 52 | * Avoid timeout too soon for frequently visited direct sites 53 | * Fix reporting malformed requests in two cases when request has body: 54 | - Authenticate requests 55 | - Error occurred before request is sent 56 | * Support multi-lined headers 57 | * Change client connection timeout to 15s 58 | * Change as direct delta to 15 59 | * Provide ARMv5 binary 60 | 61 | 0.7.6 (2013-07-28) 62 | * Fix bug for close connection response with no body 63 | * Fix response not keep alive by default 64 | * Always try parent proxy upon DNS/connection error 65 | * Do not take special handling on log with debug option 66 | * Add proxy status statistics in debug code 67 | 68 | 0.7.5 (2013-07-25) 69 | * Fix crash on IPv6 client authentication 70 | * Provide ARMv6 binary 71 | 72 | 0.7.4 (2013-07-15) 73 | * Fix adding extra connection header for client request with both 74 | "Proxy-Connection" and "Connection" headers 75 | * Ignore UTF-8 BOM in config file 76 | 77 | 0.7.3 (2013-07-10) 78 | * Handle 100-continue: do not forward expect header from client, ignore 100 79 | continue response replied by some web servers 80 | * For windows: add cow-hide.exe to run cow.exe as background process, 81 | (provided by to xupefei) 82 | * Filter sites covered by user specified domains on load 83 | * Fix incorrectly changing header value to lower case: user name and 84 | password can now contain upper case letters 85 | 86 | 0.7.2 (2013-07-01) 87 | * Close idle server connections earlier: avoid opening too many sockets 88 | * Support authenticating multiple users (can limit port for each user) 89 | 90 | 0.7.1 (2013-06-08) 91 | * Fix parent proxy fallback bug 92 | 93 | 0.7 (2013-06-07) 94 | * Always use direct connection only for private IP addresses 95 | * Support multiple HTTP/SOCKS5 parent proxies 96 | * Support running multiple ssh server 97 | * Fix client request read timeout handling 98 | * Refactor parent proxy related code 99 | 100 | 0.6.3 (2013-05-27) 101 | * Support more shadowsocks encryption method 102 | * Fix several windows network error detection issues (dirty hack) 103 | 104 | 0.6.2 (2013-05-17) 105 | * Support multiple shadowsocks servers 106 | * Simple load balancing: backup or hash strategy 107 | * PAC fix: do not add domains with blocked host/sub domain 108 | * Remove some no longer working command line options 109 | 110 | 0.6.1 (2013-03-14) 111 | 112 | * Avoid using too much memory to hold http requests 113 | * Support http parent proxy basic authentication 114 | * For windows: add cow-taskbar.exe to hide cmd window to status area 115 | * Fix timeout error detection 116 | * Some bug fixes 117 | 118 | 0.6 (2013-03-03) 119 | 120 | * Allow user to specify proxy address in PAC 121 | * Performance optimization 122 | * More tolerant with HTTP servers and clients 123 | * Some bug fixes 124 | 125 | 0.5.1 (2013-02-10) 126 | 127 | * Handle blocked site that will return EOF 128 | * Small bug fixes 129 | 130 | 0.5 (2013-02-07) 131 | 132 | * Support parent HTTP proxy (such as goagent) 133 | * Work more automatically: because of this, updateBlocked, updateDirect, 134 | autoRetry options and chou file are removed 135 | * Record direct/blocked visit count to make blocked/direct site handling 136 | more reliable 137 | * Builtin common blocked/direct site list 138 | * Periodically estimate timeout value to avoid considering direct site as 139 | blocked with bad network connection 140 | * Support specifying host in blocked/direct file 141 | * User configurable timeout 142 | * Better windows support: connection reset, timeout and DNS error detection 143 | tested and works on XP 144 | * Support listening multiple addresses 145 | * Support IP based and user password authentication 146 | * Various bug fixes 147 | 148 | 0.3.5 (2012-12-23) 149 | 150 | * Performance improvement by better buffer usage 151 | * Allow specifying config file on command line 152 | * Better windows support: Config and domain list file on windows are put in the same 153 | directory as COW's binary. And they all have txt extension for easy editing 154 | * Bug fix: convert HTTP/1.0 response to HTTP/1.1 155 | 156 | 0.3.4 (2012-12-09) 157 | 158 | * Support shadowsocks 159 | * Reduce latency (maybe just a little, not measured) 160 | * Allow specifying ssh server port in config file 161 | * Bug fix: crash when handling flush error 162 | * Bug fix: correctly handle web servers which use closed connection to 163 | indicate end of response 164 | 165 | 0.3.3 (2012-12-05) 166 | 167 | * Keep HTTP CONNECT connection open. Avoid problems for Application which 168 | uses long connection. 169 | * Bug fix: crash when printing domain list inconsistency message. 170 | -------------------------------------------------------------------------------- /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-en.md: -------------------------------------------------------------------------------- 1 | # COW (Climb Over the Wall) proxy 2 | 3 | COW is a HTTP proxy to simplify bypassing the great firewall. It tries to automatically identify blocked websites and only use parent proxy for those sites. 4 | 5 | Current version: 0.9.8 [CHANGELOG](CHANGELOG) 6 | [![Build Status](https://travis-ci.org/cyfdecyf/cow.png?branch=master)](https://travis-ci.org/cyfdecyf/cow) 7 | 8 | ## Features 9 | 10 | - As a HTTP proxy, can be used by mobile devices 11 | - Supports HTTP, SOCKS5, [shadowsocks](https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) and COW itself as parent proxy 12 | - Supports simple load balancing between multiple parent proxies 13 | - Automatically identify blocked websites, only use parent proxy for those sites 14 | - Generate and serve PAC file for browser to bypass COW for best performance 15 | - Contain domains that can be directly accessed (recorded accoring to your visit history) 16 | 17 | # Quickstart 18 | 19 | Install: 20 | 21 | - **OS X, Linux (x86, ARM):** Run the following command (also for update) 22 | 23 | curl -L git.io/cow | bash 24 | 25 | - All binaries are compiled on OS X, if ARM binary can't work, please download [Go ARM](https://storage.googleapis.com/golang/go1.6.2.linux-amd64.tar.gz) and install from source. 26 | - **Windows:** download from the [release page](https://github.com/cyfdecyf/cow/releases) 27 | - If you are familiar with Go, run `go get github.com/cyfdecyf/cow` to install from source. 28 | 29 | Modify configuration file `~/.cow/rc` (OS X or Linux) or `rc.txt` (Windows). A simple example with the most important options: 30 | 31 | # Line starting with # is comment and will be ignored 32 | # Local proxy listen address 33 | listen = http://127.0.0.1:7777 34 | 35 | # SOCKS5 parent proxy 36 | proxy = socks5://127.0.0.1:1080 37 | # HTTP parent proxy 38 | proxy = http://127.0.0.1:8080 39 | proxy = http://user:password@127.0.0.1:8080 40 | # shadowsocks parent proxy 41 | proxy = ss://aes-128-cfb:password@1.2.3.4:8388 42 | # cow parent proxy 43 | proxy = cow://aes-128-cfb:password@1.2.3.4:8388 44 | 45 | See [detailed configuration example](doc/sample-config/rc-en) for other features. 46 | 47 | The PAC file can be accessed at `http:///pac`, for the above example: `http://127.0.0.1:7777/pac`. 48 | 49 | Command line options can override options in the configuration file For more details, see the output of `cow -h` 50 | 51 | ## Blocked and directly accessible sites list 52 | 53 | In ideal situation, you don't need to specify which sites are blocked and which are not, but COW hasen't reached that goal. So you may need to manually specify this if COW made the wrong judgement. 54 | 55 | - `/blocked` for blocked sites 56 | - `/direct` for directly accessible sites 57 | - One line for each domain 58 | - `google.com` means `*.google.com` 59 | - You can use domains like `google.com.hk` 60 | 61 | # Technical details 62 | 63 | ## Visited site recording 64 | 65 | COW records all visited hosts and visit count in `stat` (which is a json file) under the same directory with config file. 66 | 67 | - **For unknown site, first try direct access, use parent proxy upon failure. After 2 minutes, try direct access again** 68 | - Builtin [common blocked site](site_blocked.go) in order to reduce time to discover blockage and the use parent proxy 69 | - Hosts will be put into PAC after a few times of successful direct visit 70 | - Hosts will use parent proxy if direct access failed for a few times 71 | - To avoid mistakes, will try direct access with some probability 72 | - Host will be deleted if not visited for a few days 73 | - Hosts under builtin/manually specified blocked and direct domains will not appear in `stat` 74 | 75 | ## How does COW detect blocked sites 76 | 77 | Upon the following error, one domain is considered to be blocked 78 | 79 | - Server connection reset 80 | - Connection to server timeout 81 | - Read from server timeout 82 | 83 | COW will retry HTTP request upon these errors, But if there's some data sent back to the client, connection with the client will be dropped to signal error.. 84 | 85 | Server connection reset is usually reliable in detecting blocked sites. But timeout is not. COW tries to estimate timeout value every 30 seconds, in order to avoid considering normal sites as blocked when network condition is bad. Revert to direct access after two minutes upon first blockage is also to avoid mistakes. 86 | 87 | If automatica timeout retry causes problem for you, try to change `readTimeout` and `dialTimeout` in configuration. 88 | 89 | # Limitations 90 | 91 | - No caching, COW just passes traffic between clients and web servers 92 | - For web browsing, browsers have their own cache 93 | - Blocked site detection is not always reliable 94 | 95 | # Acknowledgements 96 | 97 | Refer to [README.md](README.md). 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COW (Climb Over the Wall) proxy 2 | 3 | COW 是一个简化穿墙的 HTTP 代理服务器。它能自动检测被墙网站,仅对这些网站使用二级代理。 4 | 5 | [English README](README-en.md). 6 | 7 | 当前版本:0.9.8 [CHANGELOG](CHANGELOG) 8 | [![Build Status](https://travis-ci.org/cyfdecyf/cow.png?branch=master)](https://travis-ci.org/cyfdecyf/cow) 9 | 10 | **欢迎在 develop branch 进行开发并发送 pull request :)** 11 | 12 | ## 功能 13 | 14 | COW 的设计目标是自动化,理想情况下用户无需关心哪些网站无法访问,可直连网站也不会因为使用二级代理而降低访问速度。 15 | 16 | - 作为 HTTP 代理,可提供给移动设备使用;若部署在国内服务器上,可作为 APN 代理 17 | - 支持 HTTP, SOCKS5, [shadowsocks](https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) 和 cow 自身作为二级代理 18 | - 可使用多个二级代理,支持简单的负载均衡 19 | - 自动检测网站是否被墙,仅对被墙网站使用二级代理 20 | - 自动生成包含直连网站的 PAC,访问这些网站时可绕过 COW 21 | - 内置[常见可直连网站](site_direct.go),如国内社交、视频、银行、电商等网站(可手工添加) 22 | 23 | # 快速开始 24 | 25 | 安装: 26 | 27 | - **OS X, Linux (x86, ARM):** 执行以下命令(也可用于更新) 28 | 29 | curl -L git.io/cow | bash 30 | 31 | - 环境变量 `COW_INSTALLDIR` 可以指定安装的路径,若该环境变量不是目录则询问用户 32 | - 所有 binary 在 OS X 上编译获得,若 ARM 版本可能无法工作,请下载 [Go ARM](https://storage.googleapis.com/golang/go1.6.2.linux-amd64.tar.gz) 后从源码安装 33 | - **Windows:** 从 [release 页面](https://github.com/cyfdecyf/cow/releases)下载 34 | - 熟悉 Go 的用户可用 `go get github.com/cyfdecyf/cow` 从源码安装 35 | 36 | 编辑 `~/.cow/rc` (Linux) 或 `rc.txt` (Windows),简单的配置例子如下: 37 | 38 | #开头的行是注释,会被忽略 39 | # 本地 HTTP 代理地址 40 | # 配置 HTTP 和 HTTPS 代理时请填入该地址 41 | # 若配置代理时有对所有协议使用该代理的选项,且你不清楚此选项的含义,请勾选 42 | # 或者在自动代理配置中填入 http://127.0.0.1:7777/pac 43 | listen = http://127.0.0.1:7777 44 | 45 | # SOCKS5 二级代理 46 | proxy = socks5://127.0.0.1:1080 47 | # HTTP 二级代理 48 | proxy = http://127.0.0.1:8080 49 | proxy = http://user:password@127.0.0.1:8080 50 | # shadowsocks 二级代理 51 | proxy = ss://aes-128-cfb:password@1.2.3.4:8388 52 | # cow 二级代理 53 | proxy = cow://aes-128-cfb:password@1.2.3.4:8388 54 | 55 | 使用 cow 协议的二级代理需要在国外服务器上安装 COW,并使用如下配置: 56 | 57 | listen = cow://aes-128-cfb:password@0.0.0.0:8388 58 | 59 | 完成配置后启动 COW 并配置好代理即可使用。 60 | 61 | # 详细使用说明 62 | 63 | 配置文件在 Unix 系统上为 `~/.cow/rc`,Windows 上为 COW 所在目录的 `rc.txt` 文件。 **[样例配置](doc/sample-config/rc) 包含了所有选项以及详细的说明**,建议下载然后修改。 64 | 65 | 启动 COW: 66 | 67 | - Unix 系统在命令行上执行 `cow &` (若 COW 不在 `PATH` 所在目录,请执行 `./cow &`) 68 | - [Linux 启动脚本](doc/init.d/cow),如何使用请参考注释(Debian 测试通过,其他 Linux 发行版应该也可使用) 69 | - Windows 70 | - 双击 `cow-taskbar.exe`,隐藏到托盘执行 71 | - 双击 `cow-hide.exe`,隐藏为后台程序执行 72 | - 以上两者都会启动 `cow.exe` 73 | 74 | PAC url 为 `http:///pac`,也可将浏览器的 HTTP/HTTPS 代理设置为 `listen address` 使所有网站都通过 COW 访问。 75 | 76 | **使用 PAC 可获得更好的性能,但若 PAC 中某网站从直连变成被封,浏览器会依然尝试直连。遇到这种情况可以暂时不使用 PAC 而总是走 HTTP 代理,让 COW 学习到新的被封网站。** 77 | 78 | 命令行选项可以覆盖部分配置文件中的选项、打开 debug/request/reply 日志,执行 `cow -h` 来获取更多信息。 79 | 80 | ## 手动指定被墙和直连网站 81 | 82 | **一般情况下无需手工指定被墙和直连网站,该功能只是是为了处理特殊情况和性能优化。** 83 | 84 | 配置文件所在目录下的 `blocked` 和 `direct` 可指定被墙和直连网站(`direct` 中的 host 会添加到 PAC)。 85 | Windows 下文件名为 `blocked.txt` 和 `direct.txt`。 86 | 87 | - 每行一个域名或者主机名(COW 会先检查主机名是否在列表中,再检查域名) 88 | - 二级域名如 `google.com` 相当于 `*.google.com` 89 | - `com.hk`, `edu.cn` 等二级域名下的三级域名,作为二级域名处理。如 `google.com.hk` 相当于 `*.google.com.hk` 90 | - 其他三级及以上域名/主机名做精确匹配,例如 `plus.google.com` 91 | 92 | # 技术细节 93 | 94 | ## 访问网站记录 95 | 96 | COW 在配置文件所在目录下的 `stat` json 文件中记录经常访问网站被墙和直连访问的次数。 97 | 98 | - **对未知网站,先尝试直接连接,失败后使用二级代理重试请求,2 分钟后再尝试直接** 99 | - 内置[常见被墙网站](site_blocked.go),减少检测被墙所需时间(可手工添加) 100 | - 直连访问成功一定次数后相应的 host 会添加到 PAC 101 | - host 被墙一定次数后会直接用二级代理访问 102 | - 为避免误判,会以一定概率再次尝试直连访问 103 | - host 若一段时间没有访问会自动被删除(避免 `stat` 文件无限增长) 104 | - 内置网站列表和用户指定的网站不会出现在统计文件中 105 | 106 | ## COW 如何检测被墙网站 107 | 108 | COW 将以下错误认为是墙在作怪: 109 | 110 | - 服务器连接被重置 (connection reset) 111 | - 创建连接超时 112 | - 服务器读操作超时 113 | 114 | 无论是普通的 HTTP GET 等请求还是 CONNECT 请求,失败后 COW 都会自动重试请求。(如果已经有内容发送回 client 则不会重试而是直接断开连接。) 115 | 116 | 用连接被重置来判断被墙通常来说比较可靠,超时则不可靠。COW 每隔半分钟会尝试估算合适的超时间隔,避免在网络连接差的情况下把直连网站由于超时也当成被墙。 117 | COW 默认配置下检测到被墙后,过两分钟再次尝试直连也是为了避免误判。 118 | 119 | 如果超时自动重试给你造成了问题,请参考[样例配置](doc/sample-config/rc)高级选项中的 `readTimeout`, `dialTimeout` 选项。 120 | 121 | ## 限制 122 | 123 | - 不提供 cache 124 | - 不支持 HTTP pipeline(Chrome, Firefox 默认都没开启 pipeline,支持这个功能容易增加问题而好处并不明显) 125 | 126 | # 致谢 (Acknowledgements) 127 | 128 | 贡献代码: 129 | 130 | - @fzerorubigd: various bug fixes and feature implementation 131 | - @tevino: http parent proxy basic authentication 132 | - @xupefei: 提供 cow-hide.exe 以在 windows 上在后台执行 cow.exe 133 | - @sunteya: 改进启动和安装脚本 134 | 135 | Bug reporter: 136 | 137 | - GitHub users: glacjay, trawor, Blaskyy, lucifer9, zellux, xream, hieixu, fantasticfears, perrywky, JayXon, graminc, WingGao, polong, dallascao, luosheng 138 | - Twitter users: 特别感谢 @shao222 多次帮助测试新版并报告了不少 bug, @xixitalk 139 | 140 | @glacjay 对 0.3 版本的 COW 提出了让它更加自动化的建议,使我重新考虑 COW 的设计目标并且改进成 0.5 版本之后的工作方式。 141 | -------------------------------------------------------------------------------- /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 = "cow proxy" 19 | authRawBodyTmpl = ` 20 | 21 | COW Proxy 22 | 23 |

407 Proxy authentication required

24 |
25 | Generated by COW 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 = "bad1cb3526e4b257a62cda10f7c25aad" 51 | 52 | digest := calcRequestDigest(auth, a1, "GET") 53 | if digest != targetDigest { 54 | t.Errorf("authentication digest calculation wrong, got: %x, 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 | -------------------------------------------------------------------------------- /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 = "0.9.8" 20 | defaultListenAddr = "127.0.0.1:7777" 21 | defaultEstimateTarget = "example.com" 22 | ) 23 | 24 | type LoadBalanceMode byte 25 | 26 | const ( 27 | loadBalanceBackup LoadBalanceMode = iota 28 | loadBalanceHash 29 | loadBalanceLatency 30 | ) 31 | 32 | // allow the same tunnel ports as polipo 33 | var defaultTunnelAllowedPort = []string{ 34 | "22", "80", "443", // ssh, http, https 35 | "873", // rsync 36 | "143", "220", "585", "993", // imap, imap3, imap4-ssl, imaps 37 | "109", "110", "473", "995", // pop2, pop3, hybrid-pop, pop3s 38 | "5222", "5269", // jabber-client, jabber-server 39 | "2401", "3690", "9418", // cvspserver, svn, git 40 | } 41 | 42 | type Config struct { 43 | RcFile string // config file 44 | LogFile string // path for log file 45 | AlwaysProxy bool // whether we should alwyas use parent proxy 46 | LoadBalance LoadBalanceMode // select load balance mode 47 | 48 | TunnelAllowedPort map[string]bool // allowed ports to create tunnel 49 | 50 | SshServer []string 51 | 52 | // authenticate client 53 | UserPasswd string 54 | UserPasswdFile string // file that contains user:passwd:[port] pairs 55 | AllowedClient string 56 | AuthTimeout time.Duration 57 | 58 | // advanced options 59 | DialTimeout time.Duration 60 | ReadTimeout time.Duration 61 | 62 | Core int 63 | DetectSSLErr bool 64 | 65 | HttpErrorCode int 66 | 67 | dir string // directory containing config file 68 | StatFile string // Path for stat file 69 | BlockedFile string // blocked sites specified by user 70 | DirectFile string // direct sites specified by user 71 | 72 | // not configurable in config file 73 | PrintVer bool 74 | EstimateTimeout bool // Whether to run estimateTimeout(). 75 | EstimateTarget string // Timeout estimate target site. 76 | 77 | // not config option 78 | saveReqLine bool // for http and cow parent, should save request line from client 79 | } 80 | 81 | var config Config 82 | var configNeedUpgrade bool // whether should upgrade config file 83 | 84 | func printVersion() { 85 | fmt.Println("cow version", version) 86 | } 87 | 88 | func initConfig(rcFile string) { 89 | config.dir = path.Dir(rcFile) 90 | config.BlockedFile = path.Join(config.dir, blockedFname) 91 | config.DirectFile = path.Join(config.dir, directFname) 92 | config.StatFile = path.Join(config.dir, statFname) 93 | 94 | config.DetectSSLErr = false 95 | config.AlwaysProxy = false 96 | 97 | config.AuthTimeout = 2 * time.Hour 98 | config.DialTimeout = defaultDialTimeout 99 | config.ReadTimeout = defaultReadTimeout 100 | 101 | config.TunnelAllowedPort = make(map[string]bool) 102 | for _, port := range defaultTunnelAllowedPort { 103 | config.TunnelAllowedPort[port] = true 104 | } 105 | 106 | config.EstimateTarget = defaultEstimateTarget 107 | } 108 | 109 | // Whether command line options specifies listen addr 110 | var cmdHasListenAddr bool 111 | 112 | func parseCmdLineConfig() *Config { 113 | var c Config 114 | var listenAddr string 115 | 116 | flag.StringVar(&c.RcFile, "rc", "", "config file, defaults to $HOME/.cow/rc on Unix, ./rc.txt on Windows") 117 | // Specifying listen default value to StringVar would override config file options 118 | flag.StringVar(&listenAddr, "listen", "", "listen address, disables listen in config") 119 | flag.IntVar(&c.Core, "core", 2, "number of cores to use") 120 | flag.StringVar(&c.LogFile, "logFile", "", "write output to file") 121 | flag.BoolVar(&c.PrintVer, "version", false, "print version") 122 | flag.BoolVar(&c.EstimateTimeout, "estimate", true, "enable/disable estimate timeout") 123 | 124 | flag.Parse() 125 | 126 | if c.RcFile == "" { 127 | c.RcFile = getDefaultRcFile() 128 | } else { 129 | c.RcFile = expandTilde(c.RcFile) 130 | } 131 | if err := isFileExists(c.RcFile); err != nil { 132 | Fatal("fail to get config file:", err) 133 | } 134 | initConfig(c.RcFile) 135 | 136 | if listenAddr != "" { 137 | configParser{}.ParseListen(listenAddr) 138 | cmdHasListenAddr = true // must come after parse 139 | } 140 | return &c 141 | } 142 | 143 | func parseBool(v, msg string) bool { 144 | switch v { 145 | case "true": 146 | return true 147 | case "false": 148 | return false 149 | default: 150 | Fatalf("%s should be true or false\n", msg) 151 | } 152 | return false 153 | } 154 | 155 | func parseInt(val, msg string) (i int) { 156 | var err error 157 | if i, err = strconv.Atoi(val); err != nil { 158 | Fatalf("%s should be an integer\n", msg) 159 | } 160 | return 161 | } 162 | 163 | func parseDuration(val, msg string) (d time.Duration) { 164 | var err error 165 | if d, err = time.ParseDuration(val); err != nil { 166 | Fatalf("%s %v\n", msg, err) 167 | } 168 | return 169 | } 170 | 171 | func checkServerAddr(addr string) error { 172 | _, _, err := net.SplitHostPort(addr) 173 | return err 174 | } 175 | 176 | func isUserPasswdValid(val string) bool { 177 | arr := strings.SplitN(val, ":", 2) 178 | if len(arr) != 2 || arr[0] == "" || arr[1] == "" { 179 | return false 180 | } 181 | return true 182 | } 183 | 184 | // proxyParser provides functions to parse different types of parent proxy 185 | type proxyParser struct{} 186 | 187 | func (p proxyParser) ProxySocks5(val string) { 188 | if err := checkServerAddr(val); err != nil { 189 | Fatal("parent socks server", err) 190 | } 191 | parentProxy.add(newSocksParent(val)) 192 | } 193 | 194 | func (pp proxyParser) ProxyHttp(val string) { 195 | var userPasswd, server string 196 | 197 | arr := strings.Split(val, "@") 198 | if len(arr) == 1 { 199 | server = arr[0] 200 | } else if len(arr) == 2 { 201 | userPasswd = arr[0] 202 | server = arr[1] 203 | } else { 204 | Fatal("http parent proxy contains more than one @:", val) 205 | } 206 | 207 | if err := checkServerAddr(server); err != nil { 208 | Fatal("parent http server", err) 209 | } 210 | 211 | config.saveReqLine = true 212 | 213 | parent := newHttpParent(server) 214 | parent.initAuth(userPasswd) 215 | parentProxy.add(parent) 216 | } 217 | 218 | // Parse method:passwd@server:port 219 | func parseMethodPasswdServer(val string) (method, passwd, server string, err error) { 220 | // Use the right-most @ symbol to seperate method:passwd and server:port. 221 | idx := strings.LastIndex(val, "@") 222 | if idx == -1 { 223 | err = errors.New("requires both encrypt method and password") 224 | return 225 | } 226 | 227 | methodPasswd := val[:idx] 228 | server = val[idx+1:] 229 | if err = checkServerAddr(server); err != nil { 230 | return 231 | } 232 | 233 | // Password can have : inside, but I don't recommend this. 234 | arr := strings.SplitN(methodPasswd, ":", 2) 235 | if len(arr) != 2 { 236 | err = errors.New("method and password should be separated by :") 237 | return 238 | } 239 | method = arr[0] 240 | passwd = arr[1] 241 | return 242 | } 243 | 244 | // parse shadowsocks proxy 245 | func (pp proxyParser) ProxySs(val string) { 246 | method, passwd, server, err := parseMethodPasswdServer(val) 247 | if err != nil { 248 | Fatal("shadowsocks parent", err) 249 | } 250 | parent := newShadowsocksParent(server) 251 | parent.initCipher(method, passwd) 252 | parentProxy.add(parent) 253 | } 254 | 255 | func (pp proxyParser) ProxyCow(val string) { 256 | method, passwd, server, err := parseMethodPasswdServer(val) 257 | if err != nil { 258 | Fatal("cow parent", err) 259 | } 260 | 261 | if err := checkServerAddr(server); err != nil { 262 | Fatal("parent cow server", err) 263 | } 264 | 265 | config.saveReqLine = true 266 | parent := newCowParent(server, method, passwd) 267 | parentProxy.add(parent) 268 | } 269 | 270 | // listenParser provides functions to parse different types of listen addresses 271 | type listenParser struct{} 272 | 273 | func (lp listenParser) ListenHttp(val string) { 274 | if cmdHasListenAddr { 275 | return 276 | } 277 | arr := strings.Fields(val) 278 | if len(arr) > 2 { 279 | Fatal("too many fields in listen = http://", val) 280 | } 281 | 282 | var addr, addrInPAC string 283 | addr = arr[0] 284 | if len(arr) == 2 { 285 | addrInPAC = arr[1] 286 | } 287 | 288 | if err := checkServerAddr(addr); err != nil { 289 | Fatal("listen http server", err) 290 | } 291 | addListenProxy(newHttpProxy(addr, addrInPAC)) 292 | } 293 | 294 | func (lp listenParser) ListenCow(val string) { 295 | if cmdHasListenAddr { 296 | return 297 | } 298 | method, passwd, addr, err := parseMethodPasswdServer(val) 299 | if err != nil { 300 | Fatal("listen cow", err) 301 | } 302 | addListenProxy(newCowProxy(method, passwd, addr)) 303 | } 304 | 305 | // configParser provides functions to parse options in config file. 306 | type configParser struct{} 307 | 308 | func (p configParser) ParseProxy(val string) { 309 | parser := reflect.ValueOf(proxyParser{}) 310 | zeroMethod := reflect.Value{} 311 | 312 | arr := strings.Split(val, "://") 313 | if len(arr) != 2 { 314 | Fatal("proxy has no protocol specified:", val) 315 | } 316 | protocol := arr[0] 317 | 318 | methodName := "Proxy" + strings.ToUpper(protocol[0:1]) + protocol[1:] 319 | method := parser.MethodByName(methodName) 320 | if method == zeroMethod { 321 | Fatalf("no such protocol \"%s\"\n", arr[0]) 322 | } 323 | args := []reflect.Value{reflect.ValueOf(arr[1])} 324 | method.Call(args) 325 | } 326 | 327 | func (p configParser) ParseListen(val string) { 328 | if cmdHasListenAddr { 329 | return 330 | } 331 | 332 | parser := reflect.ValueOf(listenParser{}) 333 | zeroMethod := reflect.Value{} 334 | 335 | var protocol, server string 336 | arr := strings.Split(val, "://") 337 | if len(arr) == 1 { 338 | protocol = "http" 339 | server = val 340 | configNeedUpgrade = true 341 | } else { 342 | protocol = arr[0] 343 | server = arr[1] 344 | } 345 | 346 | methodName := "Listen" + strings.ToUpper(protocol[0:1]) + protocol[1:] 347 | method := parser.MethodByName(methodName) 348 | if method == zeroMethod { 349 | Fatalf("no such listen protocol \"%s\"\n", arr[0]) 350 | } 351 | args := []reflect.Value{reflect.ValueOf(server)} 352 | method.Call(args) 353 | } 354 | 355 | func (p configParser) ParseLogFile(val string) { 356 | config.LogFile = expandTilde(val) 357 | } 358 | 359 | func (p configParser) ParseAddrInPAC(val string) { 360 | configNeedUpgrade = true 361 | arr := strings.Split(val, ",") 362 | for i, s := range arr { 363 | if s == "" { 364 | continue 365 | } 366 | s = strings.TrimSpace(s) 367 | host, _, err := net.SplitHostPort(s) 368 | if err != nil { 369 | Fatal("proxy address in PAC", err) 370 | } 371 | if host == "0.0.0.0" { 372 | Fatal("can't use 0.0.0.0 as proxy address in PAC") 373 | } 374 | if hp, ok := listenProxy[i].(*httpProxy); ok { 375 | hp.addrInPAC = s 376 | } else { 377 | Fatal("can't specify address in PAC for non http proxy") 378 | } 379 | } 380 | } 381 | 382 | func (p configParser) ParseTunnelAllowedPort(val string) { 383 | arr := strings.Split(val, ",") 384 | for _, s := range arr { 385 | s = strings.TrimSpace(s) 386 | if _, err := strconv.Atoi(s); err != nil { 387 | Fatal("tunnel allowed ports", err) 388 | } 389 | config.TunnelAllowedPort[s] = true 390 | } 391 | } 392 | 393 | func (p configParser) ParseSocksParent(val string) { 394 | var pp proxyParser 395 | pp.ProxySocks5(val) 396 | configNeedUpgrade = true 397 | } 398 | 399 | func (p configParser) ParseSshServer(val string) { 400 | arr := strings.Split(val, ":") 401 | if len(arr) == 2 { 402 | val += ":22" 403 | } else if len(arr) == 3 { 404 | if arr[2] == "" { 405 | val += "22" 406 | } 407 | } else { 408 | Fatal("sshServer should be in the form of: user@server:local_socks_port[:server_ssh_port]") 409 | } 410 | // add created socks server 411 | p.ParseSocksParent("127.0.0.1:" + arr[1]) 412 | config.SshServer = append(config.SshServer, val) 413 | } 414 | 415 | var http struct { 416 | parent *httpParent 417 | serverCnt int 418 | passwdCnt int 419 | } 420 | 421 | func (p configParser) ParseHttpParent(val string) { 422 | if err := checkServerAddr(val); err != nil { 423 | Fatal("parent http server", err) 424 | } 425 | config.saveReqLine = true 426 | http.parent = newHttpParent(val) 427 | parentProxy.add(http.parent) 428 | http.serverCnt++ 429 | configNeedUpgrade = true 430 | } 431 | 432 | func (p configParser) ParseHttpUserPasswd(val string) { 433 | if !isUserPasswdValid(val) { 434 | Fatal("httpUserPassword syntax wrong, should be in the form of user:passwd") 435 | } 436 | if http.passwdCnt >= http.serverCnt { 437 | Fatal("must specify httpParent before corresponding httpUserPasswd") 438 | } 439 | http.parent.initAuth(val) 440 | http.passwdCnt++ 441 | } 442 | 443 | func (p configParser) ParseAlwaysProxy(val string) { 444 | config.AlwaysProxy = parseBool(val, "alwaysProxy") 445 | } 446 | 447 | func (p configParser) ParseLoadBalance(val string) { 448 | switch val { 449 | case "backup": 450 | config.LoadBalance = loadBalanceBackup 451 | case "hash": 452 | config.LoadBalance = loadBalanceHash 453 | case "latency": 454 | config.LoadBalance = loadBalanceLatency 455 | default: 456 | Fatalf("invalid loadBalance mode: %s\n", val) 457 | } 458 | } 459 | 460 | func (p configParser) ParseStatFile(val string) { 461 | config.StatFile = expandTilde(val) 462 | } 463 | 464 | func (p configParser) ParseBlockedFile(val string) { 465 | config.BlockedFile = expandTilde(val) 466 | if err := isFileExists(config.BlockedFile); err != nil { 467 | Fatal("blocked file:", err) 468 | } 469 | } 470 | 471 | func (p configParser) ParseDirectFile(val string) { 472 | config.DirectFile = expandTilde(val) 473 | if err := isFileExists(config.DirectFile); err != nil { 474 | Fatal("direct file:", err) 475 | } 476 | } 477 | 478 | var shadow struct { 479 | parent *shadowsocksParent 480 | passwd string 481 | method string 482 | 483 | serverCnt int 484 | passwdCnt int 485 | methodCnt int 486 | } 487 | 488 | func (p configParser) ParseShadowSocks(val string) { 489 | if shadow.serverCnt-shadow.passwdCnt > 1 { 490 | Fatal("must specify shadowPasswd for every shadowSocks server") 491 | } 492 | // create new shadowsocks parent if both server and password are given 493 | // previously 494 | if shadow.parent != nil && shadow.serverCnt == shadow.passwdCnt { 495 | if shadow.methodCnt < shadow.serverCnt { 496 | shadow.method = "" 497 | shadow.methodCnt = shadow.serverCnt 498 | } 499 | shadow.parent.initCipher(shadow.method, shadow.passwd) 500 | } 501 | if val == "" { // the final call 502 | shadow.parent = nil 503 | return 504 | } 505 | if err := checkServerAddr(val); err != nil { 506 | Fatal("shadowsocks server", err) 507 | } 508 | shadow.parent = newShadowsocksParent(val) 509 | parentProxy.add(shadow.parent) 510 | shadow.serverCnt++ 511 | configNeedUpgrade = true 512 | } 513 | 514 | func (p configParser) ParseShadowPasswd(val string) { 515 | if shadow.passwdCnt >= shadow.serverCnt { 516 | Fatal("must specify shadowSocks before corresponding shadowPasswd") 517 | } 518 | if shadow.passwdCnt+1 != shadow.serverCnt { 519 | Fatal("must specify shadowPasswd for every shadowSocks") 520 | } 521 | shadow.passwd = val 522 | shadow.passwdCnt++ 523 | } 524 | 525 | func (p configParser) ParseShadowMethod(val string) { 526 | if shadow.methodCnt >= shadow.serverCnt { 527 | Fatal("must specify shadowSocks before corresponding shadowMethod") 528 | } 529 | // shadowMethod is optional 530 | shadow.method = val 531 | shadow.methodCnt++ 532 | } 533 | 534 | func checkShadowsocks() { 535 | if shadow.serverCnt != shadow.passwdCnt { 536 | Fatal("number of shadowsocks server and password does not match") 537 | } 538 | // parse the last shadowSocks option again to initialize the last 539 | // shadowsocks server 540 | parser := configParser{} 541 | parser.ParseShadowSocks("") 542 | } 543 | 544 | // Put actual authentication related config parsing in auth.go, so config.go 545 | // doesn't need to know the details of authentication implementation. 546 | 547 | func (p configParser) ParseUserPasswd(val string) { 548 | config.UserPasswd = val 549 | if !isUserPasswdValid(config.UserPasswd) { 550 | Fatal("userPassword syntax wrong, should be in the form of user:passwd") 551 | } 552 | } 553 | 554 | func (p configParser) ParseUserPasswdFile(val string) { 555 | err := isFileExists(val) 556 | if err != nil { 557 | Fatal("userPasswdFile:", err) 558 | } 559 | config.UserPasswdFile = val 560 | } 561 | 562 | func (p configParser) ParseAllowedClient(val string) { 563 | config.AllowedClient = val 564 | } 565 | 566 | func (p configParser) ParseAuthTimeout(val string) { 567 | config.AuthTimeout = parseDuration(val, "authTimeout") 568 | } 569 | 570 | func (p configParser) ParseCore(val string) { 571 | config.Core = parseInt(val, "core") 572 | } 573 | 574 | func (p configParser) ParseHttpErrorCode(val string) { 575 | config.HttpErrorCode = parseInt(val, "httpErrorCode") 576 | } 577 | 578 | func (p configParser) ParseReadTimeout(val string) { 579 | config.ReadTimeout = parseDuration(val, "readTimeout") 580 | } 581 | 582 | func (p configParser) ParseDialTimeout(val string) { 583 | config.DialTimeout = parseDuration(val, "dialTimeout") 584 | } 585 | 586 | func (p configParser) ParseDetectSSLErr(val string) { 587 | config.DetectSSLErr = parseBool(val, "detectSSLErr") 588 | } 589 | 590 | func (p configParser) ParseEstimateTarget(val string) { 591 | config.EstimateTarget = val 592 | } 593 | 594 | // overrideConfig should contain options from command line to override options 595 | // in config file. 596 | func parseConfig(rc string, override *Config) { 597 | // fmt.Println("rcFile:", path) 598 | f, err := os.Open(expandTilde(rc)) 599 | if err != nil { 600 | Fatal("Error opening config file:", err) 601 | } 602 | 603 | IgnoreUTF8BOM(f) 604 | 605 | scanner := bufio.NewScanner(f) 606 | 607 | parser := reflect.ValueOf(configParser{}) 608 | zeroMethod := reflect.Value{} 609 | var lines []string // store lines for upgrade 610 | 611 | var n int 612 | for scanner.Scan() { 613 | lines = append(lines, scanner.Text()) 614 | 615 | n++ 616 | line := strings.TrimSpace(scanner.Text()) 617 | if line == "" || line[0] == '#' { 618 | continue 619 | } 620 | 621 | v := strings.SplitN(line, "=", 2) 622 | if len(v) != 2 { 623 | Fatal("config syntax error on line", n) 624 | } 625 | key, val := strings.TrimSpace(v[0]), strings.TrimSpace(v[1]) 626 | 627 | methodName := "Parse" + strings.ToUpper(key[0:1]) + key[1:] 628 | method := parser.MethodByName(methodName) 629 | if method == zeroMethod { 630 | Fatalf("no such option \"%s\"\n", key) 631 | } 632 | // for backward compatibility, allow empty string in shadowMethod and logFile 633 | if val == "" && key != "shadowMethod" && key != "logFile" { 634 | Fatalf("empty %s, please comment or remove unused option\n", key) 635 | } 636 | args := []reflect.Value{reflect.ValueOf(val)} 637 | method.Call(args) 638 | } 639 | if scanner.Err() != nil { 640 | Fatalf("Error reading rc file: %v\n", scanner.Err()) 641 | } 642 | f.Close() 643 | 644 | overrideConfig(&config, override) 645 | checkConfig() 646 | 647 | if configNeedUpgrade { 648 | upgradeConfig(rc, lines) 649 | } 650 | } 651 | 652 | func upgradeConfig(rc string, lines []string) { 653 | newrc := rc + ".upgrade" 654 | f, err := os.Create(newrc) 655 | if err != nil { 656 | fmt.Println("can't create upgraded config file") 657 | return 658 | } 659 | 660 | // Upgrade config. 661 | proxyId := 0 662 | listenId := 0 663 | w := bufio.NewWriter(f) 664 | for _, line := range lines { 665 | line := strings.TrimSpace(line) 666 | if line == "" || line[0] == '#' { 667 | w.WriteString(line + newLine) 668 | continue 669 | } 670 | 671 | v := strings.Split(line, "=") 672 | key := strings.TrimSpace(v[0]) 673 | 674 | switch key { 675 | case "listen": 676 | listen := listenProxy[listenId] 677 | listenId++ 678 | w.WriteString(listen.genConfig() + newLine) 679 | // comment out original 680 | w.WriteString("#" + line + newLine) 681 | case "httpParent", "shadowSocks", "socksParent": 682 | backPool, ok := parentProxy.(*backupParentPool) 683 | if !ok { 684 | panic("initial parent pool should be backup pool") 685 | } 686 | parent := backPool.parent[proxyId] 687 | proxyId++ 688 | w.WriteString(parent.genConfig() + newLine) 689 | // comment out original 690 | w.WriteString("#" + line + newLine) 691 | case "httpUserPasswd", "shadowPasswd", "shadowMethod", "addrInPAC": 692 | // just comment out 693 | w.WriteString("#" + line + newLine) 694 | case "proxy": 695 | proxyId++ 696 | w.WriteString(line + newLine) 697 | default: 698 | w.WriteString(line + newLine) 699 | } 700 | } 701 | w.Flush() 702 | f.Close() // Must close file before renaming, otherwise will fail on windows. 703 | 704 | // Rename new and old config file. 705 | if err := os.Rename(rc, rc+"0.8"); err != nil { 706 | fmt.Println("can't backup config file for upgrade:", err) 707 | return 708 | } 709 | if err := os.Rename(newrc, rc); err != nil { 710 | fmt.Println("can't rename upgraded rc to original name:", err) 711 | return 712 | } 713 | } 714 | 715 | func overrideConfig(oldconfig, override *Config) { 716 | newVal := reflect.ValueOf(override).Elem() 717 | oldVal := reflect.ValueOf(oldconfig).Elem() 718 | 719 | // typeOfT := newVal.Type() 720 | for i := 0; i < newVal.NumField(); i++ { 721 | newField := newVal.Field(i) 722 | oldField := oldVal.Field(i) 723 | // log.Printf("%d: %s %s = %v\n", i, 724 | // typeOfT.Field(i).Name, newField.Type(), newField.Interface()) 725 | switch newField.Kind() { 726 | case reflect.String: 727 | s := newField.String() 728 | if s != "" { 729 | oldField.SetString(s) 730 | } 731 | case reflect.Int: 732 | i := newField.Int() 733 | if i != 0 { 734 | oldField.SetInt(i) 735 | } 736 | } 737 | } 738 | 739 | oldconfig.EstimateTimeout = override.EstimateTimeout 740 | } 741 | 742 | // Must call checkConfig before using config. 743 | func checkConfig() { 744 | checkShadowsocks() 745 | // listenAddr must be handled first, as addrInPAC dependends on this. 746 | if listenProxy == nil { 747 | listenProxy = []Proxy{newHttpProxy(defaultListenAddr, "")} 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /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 TestTunnelAllowedPort(t *testing.T) { 27 | initConfig("") 28 | parser := configParser{} 29 | parser.ParseTunnelAllowedPort("1, 2, 3, 4, 5") 30 | parser.ParseTunnelAllowedPort("6") 31 | parser.ParseTunnelAllowedPort("7") 32 | parser.ParseTunnelAllowedPort("8") 33 | 34 | testData := []struct { 35 | port string 36 | allowed bool 37 | }{ 38 | {"80", true}, // default allowd ports 39 | {"443", true}, 40 | {"1", true}, 41 | {"3", true}, 42 | {"5", true}, 43 | {"7", true}, 44 | {"8080", false}, 45 | {"8388", false}, 46 | } 47 | 48 | for _, td := range testData { 49 | allowed := config.TunnelAllowedPort[td.port] 50 | if allowed != td.allowed { 51 | t.Errorf("port %s allowed %v, got %v\n", td.port, td.allowed, allowed) 52 | } 53 | } 54 | } 55 | 56 | func TestParseProxy(t *testing.T) { 57 | pool, ok := parentProxy.(*backupParentPool) 58 | if !ok { 59 | t.Fatal("parentPool by default should be backup pool") 60 | } 61 | cnt := -1 62 | 63 | var parser configParser 64 | parser.ParseProxy("http://127.0.0.1:8080") 65 | cnt++ 66 | 67 | hp, ok := pool.parent[cnt].ParentProxy.(*httpParent) 68 | if !ok { 69 | t.Fatal("1st http proxy parsed not as httpParent") 70 | } 71 | if hp.server != "127.0.0.1:8080" { 72 | t.Error("1st http proxy server address wrong, got:", hp.server) 73 | } 74 | 75 | parser.ParseProxy("http://user:passwd@127.0.0.2:9090") 76 | cnt++ 77 | hp, ok = pool.parent[cnt].ParentProxy.(*httpParent) 78 | if !ok { 79 | t.Fatal("2nd http proxy parsed not as httpParent") 80 | } 81 | if hp.server != "127.0.0.2:9090" { 82 | t.Error("2nd http proxy server address wrong, got:", hp.server) 83 | } 84 | if hp.authHeader == nil { 85 | t.Error("2nd http proxy server user password not parsed") 86 | } 87 | 88 | parser.ParseProxy("socks5://127.0.0.1:1080") 89 | cnt++ 90 | sp, ok := pool.parent[cnt].ParentProxy.(*socksParent) 91 | if !ok { 92 | t.Fatal("socks proxy parsed not as socksParent") 93 | } 94 | if sp.server != "127.0.0.1:1080" { 95 | t.Error("socks server address wrong, got:", sp.server) 96 | } 97 | 98 | parser.ParseProxy("ss://aes-256-cfb:foobar!@127.0.0.1:1080") 99 | cnt++ 100 | _, ok = pool.parent[cnt].ParentProxy.(*shadowsocksParent) 101 | if !ok { 102 | t.Fatal("shadowsocks proxy parsed not as shadowsocksParent") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /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 | blockedFname = "blocked" 12 | directFname = "direct" 13 | statFname = "stat" 14 | 15 | newLine = "\n" 16 | ) 17 | 18 | func getDefaultRcFile() string { 19 | return path.Join(path.Join(getUserHomeDir(), ".cow", rcFname)) 20 | } 21 | -------------------------------------------------------------------------------- /config_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | const ( 9 | rcFname = "rc.txt" 10 | blockedFname = "blocked.txt" 11 | directFname = "direct.txt" 12 | statFname = "stat.txt" 13 | 14 | newLine = "\r\n" 15 | ) 16 | 17 | func getDefaultRcFile() string { 18 | // On windows, put the configuration file in the same directory of cow executable 19 | // This is not a reliable way to detect binary directory, but it works for double click and run 20 | return path.Join(path.Dir(os.Args[0]), rcFname) 21 | } 22 | -------------------------------------------------------------------------------- /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, asDirect 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 asDirect && !config.AlwaysProxy { 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, cowConn: 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 | -------------------------------------------------------------------------------- /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 | COW uses separate goroutines to read client requests and server responses. 10 | 11 | - For each client, COW 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 COW to support HTTP pipeline. (Not very sure about this.) COW 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 COW will not in effect make them go back to wating response for each request. 22 | 23 | But this design does make COW 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/init.d/cow: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### BEGIN INIT INFO 3 | # Provides: cow 4 | # Required-Start: $network 5 | # Required-Stop: $network 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: COW: Climb Over the Wall http 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 cow defaults". 13 | 14 | # Note: this script requires sudo in order to run COW as the specified 15 | # user. Please change the following variables in order to use this script. 16 | # COW will search for rc/direct/block/stat file under user's $HOME/.cow/ directory. 17 | BIN=/usr/local/bin/cow 18 | USER=usr 19 | GROUP=grp 20 | PID_DIR=/var/run 21 | PID_FILE=$PID_DIR/cow.pid 22 | LOG_FILE=/var/log/cow 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 "cow running with PID $PID" 45 | ;; 46 | 1) 47 | echo "cow not running, remove PID file $PID_FILE" 48 | ;; 49 | 2) 50 | echo "Could not find PID file $PID_FILE, cow 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 "cow already running with PID $PID" 65 | return 0 66 | fi 67 | echo "starting cow" 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 "cow running with PID $PID" 78 | return 0 79 | } 80 | 81 | do_stop() { 82 | if check_running; then 83 | echo "stopping cow 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: cow {start|stop|restart|status}" 102 | RET_VAL=1 103 | ;; 104 | esac 105 | 106 | exit $RET_VAL 107 | -------------------------------------------------------------------------------- /doc/logrotate.d/cow: -------------------------------------------------------------------------------- 1 | /var/log/cow { 2 | rotate 4 3 | weekly 4 | compress 5 | missingok 6 | postrotate 7 | /etc/init.d/cow restart 8 | endscript 9 | } 10 | -------------------------------------------------------------------------------- /doc/osx/info.chenyufei.cow.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | info.chenyufei.cow 7 | ProgramArguments 8 | 9 | COWBINARY 10 | 11 | KeepAlive 12 | 13 | RunAtLoad 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/sample-config/rc: -------------------------------------------------------------------------------- 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:7777 11 | # 12 | # 上面的例子中,cow 生成的 PAC url 为 http://127.0.0.1:7777/pac 13 | # 配置浏览器或系统 HTTP 和 HTTPS 代理时请填入该地址 14 | # 若配置代理时有对所有协议使用该代理的选项,且你不清楚此选项的含义,请勾选 15 | # 16 | # cow (需两个 cow 服务器配合使用): 17 | # listen = cow://encrypt_method:password@1.2.3.4:5678 18 | # 19 | # 若 1.2.3.4:5678 在国外,位于国内的 cow 配置其为二级代理后,两个 cow 之间可以 20 | # 通过加密连接传输 http 代理流量。目前的加密采用与 shadowsocks 相同的方式。 21 | # 22 | # 其他说明: 23 | # - 若 server_address 为 0.0.0.0,监听本机所有 IP 地址 24 | # - 可以用如下语法指定 PAC 中返回的代理服务器地址(当使用端口映射将 http 代理提供给外网时使用) 25 | # listen = http://127.0.0.1:7777 1.2.3.4:5678 26 | # 27 | listen = http://127.0.0.1:7777 28 | 29 | # 日志文件路径,如不指定则输出到 stdout 30 | #logFile = 31 | 32 | # COW 默认仅对被墙网站使用二级代理 33 | # 下面选项设置为 true 后,所有网站都通过二级代理访问 34 | #alwaysProxy = false 35 | 36 | # 指定多个二级代理时使用的负载均衡策略,可选策略如下 37 | # 38 | # backup: 默认策略,优先使用第一个指定的二级代理,其他仅作备份使用 39 | # hash: 根据请求的 host name,优先使用 hash 到的某一个二级代理 40 | # latency: 优先选择连接延迟最低的二级代理 41 | # 42 | # 一个二级代理连接失败后会依次尝试其他二级代理 43 | # 失败的二级代理会以一定的概率再次尝试使用,因此恢复后会重新启用 44 | #loadBalance = backup 45 | 46 | ############################# 47 | # 指定二级代理 48 | ############################# 49 | 50 | # 二级代理统一使用下列语法指定: 51 | # 52 | # proxy = protocol://[authinfo@]server:port 53 | # 54 | # 重复使用 proxy 多次指定多个二级代理,backup 策略将按照二级代理出现的顺序来使用 55 | # 56 | # 目前支持的二级代理及配置举例: 57 | # 58 | # SOCKS5: 59 | # proxy = socks5://127.0.0.1:1080 60 | # 61 | # HTTP: 62 | # proxy = http://127.0.0.1:8080 63 | # proxy = http://user:password@127.0.0.1:8080 64 | # 65 | # 用户认证信息为可选项 66 | # 67 | # shadowsocks: 68 | # proxy = ss://encrypt_method:password@1.2.3.4:8388 69 | # proxy = ss://encrypt_method-auth:password@1.2.3.4:8388 70 | # 71 | # encrypt_method 添加 -auth 启用 One Time Auth 72 | # authinfo 中指定加密方法和密码,所有支持的加密方法如下: 73 | # aes-128-cfb, aes-192-cfb, aes-256-cfb, 74 | # bf-cfb, cast5-cfb, des-cfb, rc4-md5, 75 | # chacha20, salsa20, rc4, table 76 | # 推荐使用 aes-128-cfb 77 | # 78 | # cow: 79 | # proxy = cow://method:passwd@1.2.3.4:4321 80 | # 81 | # authinfo 与 shadowsocks 相同 82 | 83 | 84 | ############################# 85 | # 执行 ssh 命令创建 SOCKS5 代理 86 | ############################# 87 | 88 | # 下面的选项可以让 COW 执行 ssh 命令创建本地 SOCKS5 代理,并在 ssh 断开后重连 89 | # COW 会自动使用通过 ssh 命令创建的代理,无需再通过 proxy 选项指定 90 | # 可重复指定多个 91 | # 92 | # 注意这一功能需要系统上已有 ssh 命令,且必须使用 ssh public key authentication 93 | # 94 | # 若指定该选项,COW 将执行以下命令: 95 | # ssh -n -N -D -p 96 | # server_ssh_port 端口不指定则默认为 22 97 | # 如果要指定其他 ssh 选项,请修改 ~/.ssh/config 98 | #sshServer = user@server:local_socks_port[:server_ssh_port] 99 | 100 | ############################# 101 | # 认证 102 | ############################# 103 | 104 | # 指定允许的 IP 或者网段。网段仅支持 IPv4,可以指定 IPv6 地址,用逗号分隔多个项 105 | # 使用此选项时别忘了添加 127.0.0.1,否则本机访问也需要认证 106 | #allowedClient = 127.0.0.1, 192.168.1.0/24, 10.0.0.0/8 107 | 108 | # 要求客户端通过用户名密码认证 109 | # COW 总是先验证 IP 是否在 allowedClient 中,若不在其中再通过用户名密码认证 110 | #userPasswd = username:password 111 | 112 | # 如需指定多个用户名密码,可在下面选项指定的文件中列出,文件中每行内容如下 113 | # username:password[:port] 114 | # port 为可选项,若指定,则该用户只能从指定端口连接 COW 115 | # 注意:如有重复用户,COW 会报错退出 116 | #userPasswdFile = /path/to/file 117 | 118 | # 认证失效时间 119 | # 语法:2h3m4s 表示 2 小时 3 分钟 4 秒 120 | #authTimeout = 2h 121 | 122 | ############################# 123 | # 高级选项 124 | ############################# 125 | 126 | # 将指定的 HTTP error code 认为是被干扰,使用二级代理重试,默认为空 127 | #httpErrorCode = 128 | 129 | # 最多允许使用多少个 CPU 核 130 | #core = 2 131 | 132 | # 检测超时时间使用的网站,最好使用能快速访问的站点 133 | #estimateTarget = example.com 134 | 135 | # 允许建立隧道连接的端口,多个端口用逗号分隔,可重复多次 136 | # 默认总是允许下列服务的端口: ssh, http, https, rsync, imap, pop, jabber, cvs, git, svn 137 | # 如需允许其他端口,请用该选项添加 138 | # 限制隧道连接的端口可以防止将运行 COW 的服务器上只监听本机 ip 的服务暴露给外部 139 | #tunnelAllowedPort = 80, 443 140 | 141 | # GFW 会使 DNS 解析超时,也可能返回错误的地址,能连接但是读不到任何内容 142 | # 下面两个值改小一点可以加速检测网站是否被墙,但网络情况差时可能误判 143 | 144 | # 创建连接超时(语法跟 authTimeout 相同) 145 | #dialTimeout = 5s 146 | # 从服务器读超时 147 | #readTimeout = 5s 148 | 149 | # 基于 client 是否很快关闭连接来检测 SSL 错误,只对 Chrome 有效 150 | # (Chrome 遇到 SSL 错误会直接关闭连接,而不是让用户选择是否继续) 151 | # 可能将可直连网站误判为被墙网站,当 GFW 进行 SSL 中间人攻击时可以考虑使用 152 | #detectSSLErr = false 153 | 154 | # 修改 stat/blocked/direct 文件路径,如不指定,默认在配置文件所在目录下 155 | # 执行 cow 的用户需要有对 stat 文件所在目录的写权限才能更新 stat 文件 156 | #statFile = /stat 157 | #blockedFile = /blocked 158 | #directFile = /direct 159 | -------------------------------------------------------------------------------- /doc/sample-config/rc-en: -------------------------------------------------------------------------------- 1 | # Lines starting with "#" are comments. 2 | # 3 | # Listen address of the proxy server, repeat to specify multiple ones. 4 | # Syntax: 5 | # 6 | # listen = protocol://[optional@]server_address:server_port 7 | # 8 | # Supported protocols: 9 | # 10 | # HTTP (provides http proxy): 11 | # listen = http://127.0.0.1:7777 12 | # 13 | # The generated PAC url in the above example is http://127.0.0.1:7777/pac 14 | # 15 | # cow (need two cow servers to use this protocol): 16 | # listen = cow://encrypt_method:password@1.2.3.4:5678 17 | # 18 | # Suppose 1.2.3.4:5678 is outside your country and the network is not 19 | # disturbed, then COW running in your own country should configure it 20 | # as parent proxy. The two COW servers will use encrypted connection to 21 | # pass data. The encryption method used is the same as shadowsocks. 22 | # 23 | # Note: 24 | # - If server_address is 0.0.0.0, listen all IP addresses on the system. 25 | # - The following syntax can specify the proxy address in the generated PAC. 26 | # (Use this if you are using port forwarding to provide COW to external network.) 27 | # 28 | # listen = http://127.0.0.1:7777 1.2.3.4:5678 29 | # 30 | listen = http://127.0.0.1:7777 31 | 32 | # Log file path, defaults to stdout 33 | #logFile = 34 | 35 | # By default, COW only uses parent proxy if the site is blocked. 36 | # If the following option is true, COW will use parent proxy for all sites. 37 | #alwaysProxy = false 38 | 39 | # With multiple parent proxies, COW can employ one of the load balancing 40 | # strategies: 41 | # 42 | # backup: default policy, use the first prarent proxy in config, 43 | # the others are just backup 44 | # hash: hash to a specific parent proxy according to host name 45 | # latency: use the parent proxy with lowest connection latency 46 | # 47 | # When one parent proxy fails to connect, COW will try other parent proxies 48 | # in order. 49 | # Failed parent proxy will be tried with some probability, so they will be 50 | # used again after recovery. 51 | #loadBalance = backup 52 | 53 | ############################# 54 | # Specify parent proxy 55 | ############################# 56 | 57 | # Parent proxies are specified with a generic syntax (following RFC 3986): 58 | # 59 | # proxy = protocol://[authinfo@]server:port 60 | # 61 | # Repeat to specify multiple parent proxies. Backup load balancing will use 62 | # them in order if one fails to connect. 63 | # 64 | # Supported parent proxies and config example: 65 | # 66 | # SOCKS5: 67 | # proxy = socks5://127.0.0.1:1080 68 | # 69 | # HTTP: 70 | # proxy = http://127.0.0.1:8080 71 | # proxy = http://user:password@127.0.0.1:8080 72 | # 73 | # authinfo is optional 74 | # 75 | # shadowsocks: 76 | # proxy = ss://encrypt_method:password@1.2.3.4:8388 77 | # proxy = ss://encrypt_method-auth:password@1.2.3.4:8388 78 | # 79 | # Append -auth to encrypt_method to enable One Time Auth. 80 | # authinfo specifies encryption method and password. 81 | # Here are the supported encryption methods: 82 | # 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 | # aes-128-cfb is recommended. 88 | # 89 | # cow: 90 | # proxy = cow://method:passwd@1.2.3.4:4321 91 | # 92 | # authinfo is the same as shadowsocks parent proxy 93 | 94 | 95 | ############################# 96 | # Run ssh command to create SOCKS5 parent proxy 97 | ############################# 98 | 99 | # Note: shadowsocks is better, use it if you can. 100 | 101 | # The following option lets COW execute ssh command to create local 102 | # SOCKS5 proxy and automatically re-execute if ssh connection is closed. 103 | # The created SOCKS5 proxy will be used as a parent proxy. 104 | # The option can be repeated to create multiple SOCKS5 proxies. 105 | # 106 | # Note: requires ssh command and must use ssh public key authentication. 107 | # 108 | # COW will execute the following command if the option is given: 109 | # 110 | # ssh -n -N -D -p 111 | # 112 | # server_ssh_port defaults to 22 113 | # Please modify ~/.ssh/config to specify other ssh options 114 | #sshServer = user@server:local_socks_port[:server_ssh_port] 115 | 116 | ############################# 117 | # Authentication 118 | ############################# 119 | 120 | # Specify allowed IP address (IPv4 and IPv6) or sub-network (only IPv4). 121 | # Don't forget to specify 127.0.0.1 with this option. 122 | #allowedClient = 127.0.0.1, 192.168.1.0/24, 10.0.0.0/8 123 | 124 | # Require username and password authentication. COW always check IP in 125 | # allowedClient first, then ask for username authentication. 126 | #userPasswd = username:password 127 | 128 | # To specify multiple username and password, list all those in a file with 129 | # content like this: 130 | # 131 | # username:password[:port] 132 | # 133 | # port is optional, user can only connect from the specific port if specified. 134 | # COW will report error and exit if there's duplicated user. 135 | #userPasswdFile = /path/to/file 136 | 137 | # Time interval to keep authentication information. 138 | # Syntax: 2h3m4s means 2 hours 3 minutes 4 seconds 139 | #authTimeout = 2h 140 | 141 | ############################# 142 | # Advanced options 143 | ############################# 144 | 145 | # Take a specific HTTP error code as blocked and use parent proxy to retry. 146 | #httpErrorCode = 147 | 148 | # Maximum CPU core to use. 149 | #core = 2 150 | 151 | # cow uses this site to estimate timeout, better to use a fast website. 152 | #estimateTarget = example.com 153 | 154 | # Ports allowed to create tunnel (HTTP CONNECT method), comma separated list 155 | # or repeat to append more ports. 156 | # Ports for the following service are allowed by default: 157 | # 158 | # ssh, http, https, rsync, imap, pop, jabber, cvs, git, svn 159 | # 160 | # Limiting ports for tunneling prevents exposing internal services to outside. 161 | #tunnelAllowedPort = 80, 443 162 | 163 | # GFW may timeout DNS query, or return wrong server address which can connect 164 | # but blocks on read forever. 165 | # Decrease the following timeout values can speed up detecting blocked sites, 166 | # but may mistake normal sites as blocked. 167 | 168 | # DNS and connection timeout (same syntax with authTimeout). 169 | #dialTimeout = 5s 170 | # Read from server timeout. 171 | #readTimeout = 5s 172 | 173 | # Detect SSL error based on client close connection speed, only effective for 174 | # Chrome. 175 | # This detection is no reliable, may mistaken normal sites as blocked. 176 | # Only consider this option when GFW is making middle man attack. 177 | #detectSSLErr = false 178 | 179 | # Change the stat/blocked/direct file position, defaults to files under directory 180 | # containing rc file. 181 | # The cow user must write access to directory containing the stat file in order 182 | # to update stat. 183 | #statFile = /stat 184 | #blockedFile = /blocked 185 | #directFile = /direct 186 | -------------------------------------------------------------------------------- /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 | COW Proxy 30 | 31 |

{{.H1}}

32 | {{.Msg}} 33 |
34 | Generated by COW ` + 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 | -------------------------------------------------------------------------------- /estimate_timeout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "time" 8 | ) 9 | 10 | // For once blocked site, use min dial/read timeout to make switching to 11 | // parent proxy faster. 12 | const minDialTimeout = 3 * time.Second 13 | const minReadTimeout = 4 * time.Second 14 | const defaultDialTimeout = 5 * time.Second 15 | const defaultReadTimeout = 5 * time.Second 16 | const maxTimeout = 15 * time.Second 17 | 18 | var dialTimeout = defaultDialTimeout 19 | var readTimeout = defaultReadTimeout 20 | 21 | // estimateTimeout tries to fetch a url and adjust timeout value according to 22 | // how much time is spent on connect and fetch. This avoids incorrectly 23 | // considering non-blocked sites as blocked when network connection is bad. 24 | func estimateTimeout(host string, payload []byte) { 25 | //debug.Println("estimating timeout") 26 | buf := connectBuf.Get() 27 | defer connectBuf.Put(buf) 28 | var est time.Duration 29 | start := time.Now() 30 | c, err := net.Dial("tcp", host+":80") 31 | if err != nil { 32 | errl.Printf("estimateTimeout: can't connect to %s: %v, network has problem?\n", 33 | host, err) 34 | goto onErr 35 | } 36 | defer c.Close() 37 | 38 | est = time.Now().Sub(start) * 5 39 | // debug.Println("estimated dialTimeout:", est) 40 | if est > maxTimeout { 41 | est = maxTimeout 42 | } 43 | if est > config.DialTimeout { 44 | dialTimeout = est 45 | debug.Println("new dial timeout:", dialTimeout) 46 | } else if dialTimeout != config.DialTimeout { 47 | dialTimeout = config.DialTimeout 48 | debug.Println("new dial timeout:", dialTimeout) 49 | } 50 | 51 | start = time.Now() 52 | // include time spent on sending request, reading all content to make it a 53 | // little longer 54 | 55 | if _, err = c.Write(payload); err != nil { 56 | errl.Println("estimateTimeout: error sending request:", err) 57 | goto onErr 58 | } 59 | for err == nil { 60 | _, err = c.Read(buf) 61 | } 62 | if err != io.EOF { 63 | errl.Printf("estimateTimeout: error getting %s: %v, network has problem?\n", 64 | host, err) 65 | goto onErr 66 | } 67 | est = time.Now().Sub(start) * 10 68 | // debug.Println("estimated read timeout:", est) 69 | if est > maxTimeout { 70 | est = maxTimeout 71 | } 72 | if est > time.Duration(config.ReadTimeout) { 73 | readTimeout = est 74 | debug.Println("new read timeout:", readTimeout) 75 | } else if readTimeout != config.ReadTimeout { 76 | readTimeout = config.ReadTimeout 77 | debug.Println("new read timeout:", readTimeout) 78 | } 79 | return 80 | onErr: 81 | dialTimeout += 2 * time.Second 82 | readTimeout += 2 * time.Second 83 | } 84 | 85 | func runEstimateTimeout() { 86 | const estimateReq = "GET / HTTP/1.1\r\n" + 87 | "Host: %s\r\n" + 88 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:11.0) Gecko/20100101 Firefox/11.0\r\n" + 89 | "Accept: */*\r\n" + 90 | "Accept-Language: en-us,en;q=0.5\r\n" + 91 | "Accept-Encoding: gzip, deflate\r\n" + 92 | "Connection: close\r\n\r\n" 93 | 94 | readTimeout = config.ReadTimeout 95 | dialTimeout = config.DialTimeout 96 | 97 | payload := []byte(fmt.Sprintf(estimateReq, config.EstimateTarget)) 98 | 99 | for { 100 | estimateTimeout(config.EstimateTarget, payload) 101 | time.Sleep(time.Minute) 102 | } 103 | } 104 | 105 | // Guess network status based on doing HTTP request to estimateSite 106 | func networkBad() bool { 107 | return (readTimeout != config.ReadTimeout) || 108 | (dialTimeout != config.DialTimeout) 109 | } 110 | -------------------------------------------------------------------------------- /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, cow 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 | tryCnt byte 66 | } 67 | 68 | // Assume keep-alive request by default. 69 | var zeroRequest = Request{Header: Header{ConnectionKeepAlive: true}} 70 | 71 | func (r *Request) reset() { 72 | b := r.rawByte 73 | raw := r.raw 74 | *r = zeroRequest // reset to zero value 75 | 76 | if raw != nil { 77 | raw.Reset() 78 | r.rawByte = b 79 | r.raw = raw 80 | } else { 81 | r.rawByte = httpBuf.Get() 82 | r.raw = bytes.NewBuffer(r.rawByte[:0]) // must use 0 length slice 83 | } 84 | } 85 | 86 | func (r *Request) String() (s string) { 87 | return fmt.Sprintf("%s %s%s", r.Method, r.URL.HostPort, r.URL.Path) 88 | } 89 | 90 | func (r *Request) Verbose() []byte { 91 | var rqbyte []byte 92 | if r.isConnect { 93 | rqbyte = r.rawBeforeBody() 94 | } else { 95 | // This includes client request line if has http parent proxy 96 | rqbyte = r.raw.Bytes() 97 | } 98 | return rqbyte 99 | } 100 | 101 | // Message body in request is signaled by the inclusion of a Content-Length 102 | // or Transfer-Encoding header. 103 | // Refer to http://stackoverflow.com/a/299696/306935 104 | func (r *Request) hasBody() bool { 105 | return r.Chunking || r.ContLen > 0 106 | } 107 | 108 | func (r *Request) isRetry() bool { 109 | return r.tryCnt > 1 110 | } 111 | 112 | func (r *Request) tryOnce() { 113 | r.tryCnt++ 114 | } 115 | 116 | func (r *Request) tooManyRetry() bool { 117 | return r.tryCnt > 3 118 | } 119 | 120 | func (r *Request) responseNotSent() bool { 121 | return r.state <= rsSent 122 | } 123 | 124 | func (r *Request) hasSent() bool { 125 | return r.state >= rsSent 126 | } 127 | 128 | func (r *Request) releaseBuf() { 129 | if r.raw != nil { 130 | httpBuf.Put(r.rawByte) 131 | r.rawByte = nil 132 | r.raw = nil 133 | } 134 | } 135 | 136 | // rawRequest returns the raw request that can be sent directly to HTTP/1.1 server. 137 | func (r *Request) rawRequest() []byte { 138 | return r.raw.Bytes()[r.reqLnStart:] 139 | } 140 | 141 | func (r *Request) rawBeforeBody() []byte { 142 | return r.raw.Bytes()[:r.bodyStart] 143 | } 144 | 145 | func (r *Request) rawHeaderBody() []byte { 146 | return r.raw.Bytes()[r.headStart:] 147 | } 148 | 149 | func (r *Request) rawBody() []byte { 150 | return r.raw.Bytes()[r.bodyStart:] 151 | } 152 | 153 | func (r *Request) proxyRequestLine() []byte { 154 | return r.raw.Bytes()[0:r.reqLnStart] 155 | } 156 | 157 | func (r *Request) genRequestLine() { 158 | // Generate normal HTTP request line 159 | r.raw.WriteString(r.Method + " ") 160 | if len(r.URL.Path) == 0 { 161 | r.raw.WriteString("/") 162 | } else { 163 | r.raw.WriteString(r.URL.Path) 164 | } 165 | r.raw.WriteString(" HTTP/1.1\r\n") 166 | } 167 | 168 | type Response struct { 169 | Status int 170 | Reason []byte 171 | 172 | Header 173 | 174 | raw *bytes.Buffer 175 | rawByte []byte 176 | } 177 | 178 | var zeroResponse = Response{Header: Header{ConnectionKeepAlive: true}} 179 | 180 | func (rp *Response) reset() { 181 | b := rp.rawByte 182 | raw := rp.raw 183 | *rp = zeroResponse 184 | 185 | if raw != nil { 186 | raw.Reset() 187 | rp.rawByte = b 188 | rp.raw = raw 189 | } else { 190 | rp.rawByte = httpBuf.Get() 191 | rp.raw = bytes.NewBuffer(rp.rawByte[:0]) 192 | } 193 | } 194 | 195 | func (rp *Response) releaseBuf() { 196 | if rp.raw != nil { 197 | httpBuf.Put(rp.rawByte) 198 | rp.rawByte = nil 199 | rp.raw = nil 200 | } 201 | } 202 | 203 | func (rp *Response) rawResponse() []byte { 204 | return rp.raw.Bytes() 205 | } 206 | 207 | func (rp *Response) genStatusLine() { 208 | rp.raw.Write([]byte("HTTP/1.1 ")) 209 | rp.raw.WriteString(strconv.Itoa(rp.Status)) 210 | if len(rp.Reason) != 0 { 211 | rp.raw.WriteByte(' ') 212 | rp.raw.Write(rp.Reason) 213 | } 214 | rp.raw.Write([]byte(CRLF)) 215 | return 216 | } 217 | 218 | func (rp *Response) String() string { 219 | return fmt.Sprintf("%d %s", rp.Status, rp.Reason) 220 | } 221 | 222 | func (rp *Response) Verbose() []byte { 223 | return rp.raw.Bytes() 224 | } 225 | 226 | type URL struct { 227 | HostPort string // must contain port 228 | Host string // no port 229 | Port string 230 | Domain string 231 | Path string 232 | } 233 | 234 | func (url *URL) String() string { 235 | return url.HostPort + url.Path 236 | } 237 | 238 | // Set all fields according to hostPort except Path. 239 | func (url *URL) ParseHostPort(hostPort string) { 240 | if hostPort == "" { 241 | return 242 | } 243 | host, port, err := net.SplitHostPort(hostPort) 244 | if err != nil { 245 | // Add default 80 and split again. If there's still error this time, 246 | // it's not because lack of port number. 247 | host = hostPort 248 | port = "80" 249 | hostPort = net.JoinHostPort(hostPort, port) 250 | } 251 | 252 | url.Host = host 253 | url.Port = port 254 | url.HostPort = hostPort 255 | url.Domain = host2Domain(host) 256 | } 257 | 258 | // net.ParseRequestURI will unescape encoded path, but the proxy doesn't need 259 | // that. Assumes the input rawurl is valid. Even if rawurl is not valid, net.Dial 260 | // will check the correctness of the host. 261 | 262 | func ParseRequestURI(rawurl string) (*URL, error) { 263 | return ParseRequestURIBytes([]byte(rawurl)) 264 | } 265 | 266 | func ParseRequestURIBytes(rawurl []byte) (*URL, error) { 267 | if rawurl[0] == '/' { 268 | return &URL{Path: string(rawurl)}, nil 269 | } 270 | 271 | var rest, scheme []byte 272 | id := bytes.Index(rawurl, []byte("://")) 273 | if id == -1 { 274 | rest = rawurl 275 | scheme = []byte("http") // default to http 276 | } else { 277 | scheme = rawurl[:id] 278 | ASCIIToLowerInplace(scheme) // it's ok to lower case scheme 279 | if !bytes.Equal(scheme, []byte("http")) && !bytes.Equal(scheme, []byte("https")) { 280 | errl.Printf("%s protocol not supported\n", scheme) 281 | return nil, errors.New("protocol not supported") 282 | } 283 | rest = rawurl[id+3:] 284 | } 285 | 286 | var hostport, host, port, path string 287 | id = bytes.IndexByte(rest, '/') 288 | if id == -1 { 289 | hostport = string(rest) 290 | } else { 291 | hostport = string(rest[:id]) 292 | path = string(rest[id:]) 293 | } 294 | 295 | // Must add port in host so it can be used as key to find the correct 296 | // server connection. 297 | // e.g. google.com:80 and google.com:443 should use different connections. 298 | host, port, err := net.SplitHostPort(hostport) 299 | if err != nil { // missing port 300 | host = hostport 301 | if len(scheme) == 4 { 302 | hostport = net.JoinHostPort(host, "80") 303 | port = "80" 304 | } else { 305 | hostport = net.JoinHostPort(host, "443") 306 | port = "443" 307 | } 308 | } 309 | // Fixed wechat image url bug, url like http://[::ffff:183.192.196.102]/mmsns/lVxxxxxx 310 | host = strings.TrimSuffix(strings.TrimPrefix(host, "[::ffff:"), "]") 311 | hostport = net.JoinHostPort(host, port) 312 | return &URL{hostport, host, port, host2Domain(host), path}, nil 313 | } 314 | 315 | // headers of interest to a proxy 316 | // Define them as constant and use editor's completion to avoid typos. 317 | // Note RFC2616 only says about "Connection", no "Proxy-Connection", but 318 | // Firefox and Safari send this header along with "Connection" header. 319 | // See more at http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/web-proxy-connection-header.html 320 | const ( 321 | headerConnection = "connection" 322 | headerContentLength = "content-length" 323 | headerExpect = "expect" 324 | headerHost = "host" 325 | headerKeepAlive = "keep-alive" 326 | headerProxyAuthenticate = "proxy-authenticate" 327 | headerProxyAuthorization = "proxy-authorization" 328 | headerProxyConnection = "proxy-connection" 329 | headerReferer = "referer" 330 | headerTE = "te" 331 | headerTrailer = "trailer" 332 | headerTransferEncoding = "transfer-encoding" 333 | headerUpgrade = "upgrade" 334 | 335 | fullHeaderConnectionKeepAlive = "Connection: keep-alive\r\n" 336 | fullHeaderConnectionClose = "Connection: close\r\n" 337 | fullHeaderTransferEncoding = "Transfer-Encoding: chunked\r\n" 338 | ) 339 | 340 | // Using Go's method expression 341 | var headerParser = map[string]HeaderParserFunc{ 342 | headerConnection: (*Header).parseConnection, 343 | headerContentLength: (*Header).parseContentLength, 344 | headerExpect: (*Header).parseExpect, 345 | headerHost: (*Header).parseHost, 346 | headerKeepAlive: (*Header).parseKeepAlive, 347 | headerProxyAuthorization: (*Header).parseProxyAuthorization, 348 | headerProxyConnection: (*Header).parseConnection, 349 | headerTransferEncoding: (*Header).parseTransferEncoding, 350 | headerTrailer: (*Header).parseTrailer, 351 | } 352 | 353 | var hopByHopHeader = map[string]bool{ 354 | headerConnection: true, 355 | headerKeepAlive: true, 356 | headerProxyAuthorization: true, 357 | headerProxyConnection: true, 358 | headerTE: true, 359 | headerTrailer: true, 360 | headerTransferEncoding: true, 361 | headerUpgrade: true, 362 | } 363 | 364 | // Note: Value bytes passed to header parse function are in the buffer 365 | // associated with bufio and will be modified. It will also be stored in the 366 | // raw request buffer, so becareful when modifying the value bytes. (Change 367 | // case only when the spec says it is case insensitive.) 368 | // 369 | // If Header needs to hold raw value, make a copy. For example, 370 | // parseProxyAuthorization does this. 371 | 372 | type HeaderParserFunc func(*Header, []byte) error 373 | 374 | // Used by both "Connection" and "Proxy-Connection" header. COW always adds 375 | // connection header at the end of a request/response (in parseRequest and 376 | // parseResponse), no matter whether the original one has this header or not. 377 | // This will change the order of headers, but should be OK as RFC2616 4.2 says 378 | // header order is not significant. (Though general-header first is "good- 379 | // practice".) 380 | func (h *Header) parseConnection(s []byte) error { 381 | ASCIIToLowerInplace(s) 382 | h.ConnectionKeepAlive = !bytes.Contains(s, []byte("close")) 383 | return nil 384 | } 385 | 386 | func (h *Header) parseContentLength(s []byte) (err error) { 387 | h.ContLen, err = ParseIntFromBytes(s, 10) 388 | return err 389 | } 390 | 391 | func (h *Header) parseHost(s []byte) (err error) { 392 | if h.Host == "" { 393 | h.Host = string(s) 394 | } 395 | return 396 | } 397 | 398 | func (h *Header) parseKeepAlive(s []byte) (err error) { 399 | ASCIIToLowerInplace(s) 400 | id := bytes.Index(s, []byte("timeout=")) 401 | if id != -1 { 402 | id += len("timeout=") 403 | end := id 404 | for ; end < len(s) && IsDigit(s[end]); end++ { 405 | } 406 | delta, err := ParseIntFromBytes(s[id:end], 10) 407 | if err != nil { 408 | return err // possible empty bytes 409 | } 410 | h.KeepAlive = time.Second * time.Duration(delta) 411 | } 412 | return nil 413 | } 414 | 415 | func (h *Header) parseProxyAuthorization(s []byte) error { 416 | h.ProxyAuthorization = string(s) 417 | return nil 418 | } 419 | 420 | func (h *Header) parseTransferEncoding(s []byte) error { 421 | ASCIIToLowerInplace(s) 422 | // For transfer-encoding: identify, it's the same as specifying neither 423 | // content-length nor transfer-encoding. 424 | h.Chunking = bytes.Contains(s, []byte("chunked")) 425 | if !h.Chunking && !bytes.Contains(s, []byte("identity")) { 426 | return fmt.Errorf("invalid transfer encoding: %s", s) 427 | } 428 | return nil 429 | } 430 | 431 | // RFC 2616 3.6.1 states when trailers are allowed: 432 | // 433 | // a) request includes TE header 434 | // b) server is the original server 435 | // 436 | // Even though COW removes TE header, the original server can still respond 437 | // with Trailer header. 438 | // As Trailer is general header, it's possible to appear in request. But is 439 | // there any client does this? 440 | func (h *Header) parseTrailer(s []byte) error { 441 | // use errl to test if this header is common to see 442 | errl.Printf("got Trailer header: %s\n", s) 443 | if len(s) != 0 { 444 | h.Trailer = true 445 | } 446 | return nil 447 | } 448 | 449 | // For now, COW does not fully support 100-continue. It will return "417 450 | // expectation failed" if a request contains expect header. This is one of the 451 | // strategies supported by polipo, which is easiest to implement in cow. 452 | // TODO If we see lots of expect 100-continue usage, provide full support. 453 | 454 | func (h *Header) parseExpect(s []byte) error { 455 | ASCIIToLowerInplace(s) 456 | errl.Printf("Expect header: %s\n", s) // put here to see if expect header is widely used 457 | h.ExpectContinue = true 458 | /* 459 | if bytes.Contains(s, []byte("100-continue")) { 460 | h.ExpectContinue = true 461 | } 462 | */ 463 | return nil 464 | } 465 | 466 | func splitHeader(s []byte) (name, val []byte, err error) { 467 | i := bytes.IndexByte(s, ':') 468 | if i < 0 { 469 | return nil, nil, fmt.Errorf("malformed header: %#v", string(s)) 470 | } 471 | // Do not lower case field value, as it maybe case sensitive 472 | return ASCIIToLower(s[:i]), TrimSpace(s[i+1:]), nil 473 | } 474 | 475 | // Learned from net.textproto. One difference is that this one keeps the 476 | // ending '\n' in the returned line. Buf if there's only CRLF in the line, 477 | // return nil for the line. 478 | func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) { 479 | // feedly.com request headers contains things like: 480 | // "$Authorization.feedly: $FeedlyAuth\r\n", so we must test for only 481 | // continuation spaces. 482 | isspace := func(b byte) bool { 483 | return b == ' ' || b == '\t' 484 | } 485 | 486 | // Read the first line. 487 | line, err := r.ReadSlice('\n') 488 | if err != nil { 489 | return nil, err 490 | } 491 | 492 | // There are servers that use \n for line ending, so trim first before check ending. 493 | // For example, the 404 page for http://plan9.bell-labs.com/magic/man2html/1/2l 494 | trimmed := TrimSpace(line) 495 | if len(trimmed) == 0 { 496 | if len(line) > 2 { 497 | return nil, fmt.Errorf("malformed end of headers, len: %d, %#v", len(line), string(line)) 498 | } 499 | return nil, nil 500 | } 501 | 502 | if isspace(line[0]) { 503 | return nil, fmt.Errorf("malformed header, start with space: %#v", string(line)) 504 | } 505 | 506 | // Optimistically assume that we have started to buffer the next line 507 | // and it starts with an ASCII letter (the next header key), so we can 508 | // avoid copying that buffered data around in memory and skipping over 509 | // non-existent whitespace. 510 | if r.Buffered() > 0 { 511 | peek, err := r.Peek(1) 512 | if err == nil && !isspace(peek[0]) { 513 | return line, nil 514 | } 515 | } 516 | 517 | var buf []byte 518 | buf = append(buf, trimmed...) 519 | 520 | // Read continuation lines. 521 | for skipSpace(r) > 0 { 522 | line, err := r.ReadSlice('\n') 523 | if err != nil { 524 | break 525 | } 526 | buf = append(buf, ' ') 527 | buf = append(buf, TrimTrailingSpace(line)...) 528 | } 529 | buf = append(buf, '\r', '\n') 530 | return buf, nil 531 | } 532 | 533 | func skipSpace(r *bufio.Reader) int { 534 | n := 0 535 | for { 536 | c, err := r.ReadByte() 537 | if err != nil { 538 | // Bufio will keep err until next read. 539 | break 540 | } 541 | if c != ' ' && c != '\t' { 542 | r.UnreadByte() 543 | break 544 | } 545 | n++ 546 | } 547 | return n 548 | } 549 | 550 | // Only add headers that are of interest for a proxy into request/response's header map. 551 | func (h *Header) parseHeader(reader *bufio.Reader, raw *bytes.Buffer, url *URL) (err error) { 552 | h.ContLen = -1 553 | for { 554 | var line, name, val []byte 555 | if line, err = readContinuedLineSlice(reader); err != nil || len(line) == 0 { 556 | return 557 | } 558 | if name, val, err = splitHeader(line); err != nil { 559 | errl.Printf("split header %v\nline: %s\nraw header:\n%s\n", err, line, raw.Bytes()) 560 | return 561 | } 562 | // Wait Go to solve/provide the string<->[]byte optimization 563 | kn := string(name) 564 | if parseFunc, ok := headerParser[kn]; ok { 565 | if len(val) == 0 { 566 | continue 567 | } 568 | if err = parseFunc(h, val); err != nil { 569 | errl.Printf("parse header %v\nline: %s\nraw header:\n%s\n", err, line, raw.Bytes()) 570 | return 571 | } 572 | } 573 | if hopByHopHeader[kn] { 574 | continue 575 | } 576 | raw.Write(line) 577 | // debug.Printf("len %d %s", len(s), s) 578 | } 579 | } 580 | 581 | // Parse the request line and header, does not touch body 582 | func parseRequest(c *clientConn, r *Request) (err error) { 583 | var s []byte 584 | reader := c.bufRd 585 | c.setReadTimeout("parseRequest") 586 | // parse request line 587 | if s, err = reader.ReadSlice('\n'); err != nil { 588 | if isErrTimeout(err) { 589 | return errClientTimeout 590 | } 591 | return err 592 | } 593 | c.unsetReadTimeout("parseRequest") 594 | // debug.Printf("Request line %s", s) 595 | 596 | r.reset() 597 | if config.saveReqLine { 598 | r.raw.Write(s) 599 | r.reqLnStart = len(s) 600 | } 601 | 602 | var f [][]byte 603 | // Tolerate with multiple spaces and '\t' is achieved by FieldsN. 604 | if f = FieldsN(s, 3); len(f) != 3 { 605 | return fmt.Errorf("malformed request line: %#v", string(s)) 606 | } 607 | ASCIIToUpperInplace(f[0]) 608 | r.Method = string(f[0]) 609 | 610 | // Parse URI into host and path 611 | r.URL, err = ParseRequestURIBytes(f[1]) 612 | if err != nil { 613 | return 614 | } 615 | r.Header.Host = r.URL.HostPort // If Header.Host is set, parseHost will just return. 616 | if r.Method == "CONNECT" { 617 | r.isConnect = true 618 | if bool(dbgRq) && verbose && !config.saveReqLine { 619 | r.raw.Write(s) 620 | } 621 | } else { 622 | r.genRequestLine() 623 | } 624 | r.headStart = r.raw.Len() 625 | 626 | // Read request header. 627 | if err = r.parseHeader(reader, r.raw, r.URL); err != nil { 628 | errl.Printf("parse request header: %v %s\n%s", err, r, r.Verbose()) 629 | return err 630 | } 631 | if r.Chunking { 632 | r.raw.WriteString(fullHeaderTransferEncoding) 633 | } 634 | if r.ConnectionKeepAlive { 635 | r.raw.WriteString(fullHeaderConnectionKeepAlive) 636 | } else { 637 | r.raw.WriteString(fullHeaderConnectionClose) 638 | } 639 | // The spec says proxy must add Via header. polipo disables this by 640 | // default, and I don't want to let others know the user is using COW, so 641 | // don't add it. 642 | r.raw.WriteString(CRLF) 643 | r.bodyStart = r.raw.Len() 644 | return 645 | } 646 | 647 | // If an http response may have message body 648 | func (rp *Response) hasBody(method string) bool { 649 | if method == "HEAD" || rp.Status == 304 || rp.Status == 204 || 650 | rp.Status < 200 { 651 | return false 652 | } 653 | return true 654 | } 655 | 656 | // Parse response status and headers. 657 | func parseResponse(sv *serverConn, r *Request, rp *Response) (err error) { 658 | var s []byte 659 | reader := sv.bufRd 660 | if sv.maybeFake() { 661 | sv.setReadTimeout("parseResponse") 662 | } 663 | if s, err = reader.ReadSlice('\n'); err != nil { 664 | // err maybe timeout caused by explicity setting deadline, EOF, or 665 | // reset caused by GFW. 666 | debug.Printf("read response status line %v %v\n", err, r) 667 | // Server connection with error will not be used any more, so no need 668 | // to unset timeout. 669 | // For read error, return directly in order to identify whether this 670 | // is caused by GFW. 671 | return err 672 | } 673 | if sv.maybeFake() { 674 | sv.unsetReadTimeout("parseResponse") 675 | } 676 | // debug.Printf("Response line %s", s) 677 | 678 | // response status line parsing 679 | var f [][]byte 680 | if f = FieldsN(s, 3); len(f) < 2 { // status line are separated by SP 681 | return fmt.Errorf("malformed response status line: %#v %v", string(s), r) 682 | } 683 | status, err := ParseIntFromBytes(f[1], 10) 684 | 685 | rp.reset() 686 | rp.Status = int(status) 687 | if err != nil { 688 | return fmt.Errorf("response status not valid: %s len=%d %v", f[1], len(f[1]), err) 689 | } 690 | if len(f) == 3 { 691 | rp.Reason = f[2] 692 | } 693 | 694 | proto := f[0] 695 | if !bytes.Equal(proto[0:7], []byte("HTTP/1.")) { 696 | return fmt.Errorf("invalid response status line: %s request %v", string(f[0]), r) 697 | } 698 | if proto[7] == '1' { 699 | rp.raw.Write(s) 700 | } else if proto[7] == '0' { 701 | // Should return HTTP version as 1.1 to client since closed connection 702 | // will be converted to chunked encoding 703 | rp.genStatusLine() 704 | } else { 705 | return fmt.Errorf("response protocol not supported: %s", f[0]) 706 | } 707 | 708 | if err = rp.parseHeader(reader, rp.raw, r.URL); err != nil { 709 | errl.Printf("parse response header: %v %s\n%s", err, r, rp.Verbose()) 710 | return err 711 | } 712 | 713 | //Check for http error code from config file 714 | if config.HttpErrorCode > 0 && rp.Status == config.HttpErrorCode { 715 | debug.Println("Requested http code is raised") 716 | return CustomHttpErr 717 | } 718 | 719 | if rp.Status == statusCodeContinue && !r.ExpectContinue { 720 | // not expecting 100-continue, just ignore it and read final response 721 | errl.Println("Ignore server 100 response for", r) 722 | return parseResponse(sv, r, rp) 723 | } 724 | 725 | if rp.Chunking { 726 | rp.raw.WriteString(fullHeaderTransferEncoding) 727 | } else if rp.ContLen == -1 { 728 | // No chunk, no content length, assume close to signal end. 729 | rp.ConnectionKeepAlive = false 730 | if rp.hasBody(r.Method) { 731 | // Connection close, no content length specification. 732 | // Use chunked encoding to pass content back to client. 733 | debug.Println("add chunked encoding to close connection response", r, rp) 734 | rp.raw.WriteString(fullHeaderTransferEncoding) 735 | } else { 736 | debug.Println("add content-length 0 to close connection response", r, rp) 737 | rp.raw.WriteString("Content-Length: 0\r\n") 738 | } 739 | } 740 | // Whether COW should respond with keep-alive depends on client request, 741 | // not server response. 742 | if r.ConnectionKeepAlive { 743 | rp.raw.WriteString(fullHeaderConnectionKeepAlive) 744 | rp.raw.WriteString(fullKeepAliveHeader) 745 | } else { 746 | rp.raw.WriteString(fullHeaderConnectionClose) 747 | } 748 | rp.raw.WriteString(CRLF) 749 | 750 | return nil 751 | } 752 | 753 | func unquote(s string) string { 754 | return strings.Trim(s, "\"") 755 | } 756 | 757 | func parseKeyValueList(str string) map[string]string { 758 | list := strings.Split(str, ",") 759 | if len(list) == 1 && list[0] == "" { 760 | return nil 761 | } 762 | res := make(map[string]string) 763 | for _, ele := range list { 764 | kv := strings.SplitN(strings.TrimSpace(ele), "=", 2) 765 | if len(kv) != 2 { 766 | errl.Println("no equal sign in key value list element:", ele) 767 | return nil 768 | } 769 | key, val := kv[0], unquote(kv[1]) 770 | res[key] = val 771 | } 772 | return res 773 | } 774 | -------------------------------------------------------------------------------- /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 | // I'm really tired of typing google.com ... 17 | {"http://www.g.com", &URL{"www.g.com:80", "www.g.com", "80", "g.com", ""}}, 18 | {"http://plus.g.com/", &URL{"plus.g.com:80", "plus.g.com", "80", "g.com", "/"}}, 19 | {"https://g.com:80", &URL{"g.com:80", "g.com", "80", "g.com", ""}}, 20 | {"http://mail.g.com:80/", &URL{"mail.g.com:80", "mail.g.com", "80", "g.com", "/"}}, 21 | {"http://g.com:80/ncr", &URL{"g.com:80", "g.com", "80", "g.com", "/ncr"}}, 22 | {"https://g.com/ncr/tree", &URL{"g.com:443", "g.com", "443", "g.com", "/ncr/tree"}}, 23 | {"www.g.com.hk:80/", &URL{"www.g.com.hk:80", "www.g.com.hk", "80", "g.com.hk", "/"}}, 24 | {"g.com.jp:80", &URL{"g.com.jp:80", "g.com.jp", "80", "g.com.jp", ""}}, 25 | {"g.com", &URL{"g.com:80", "g.com", "80", "g.com", ""}}, 26 | {"g.com:8000/ncr", &URL{"g.com:8000", "g.com", "8000", "g.com", "/ncr"}}, 27 | {"g.com/ncr/tree", &URL{"g.com:80", "g.com", "80", "g.com", "/ncr/tree"}}, 28 | {"simplehost", &URL{"simplehost:80", "simplehost", "80", "", ""}}, 29 | {"simplehost:8080", &URL{"simplehost:8080", "simplehost", "8080", "", ""}}, 30 | {"192.168.1.1:8080/", &URL{"192.168.1.1:8080", "192.168.1.1", "8080", "", "/"}}, 31 | {"/helloworld", &URL{"", "", "", "", "/helloworld"}}, 32 | } 33 | for _, td := range testData { 34 | url, err := ParseRequestURI(td.rawurl) 35 | if url == nil { 36 | if err == nil { 37 | t.Error("nil URL must report error") 38 | } 39 | if td.url != nil { 40 | t.Error(td.rawurl, "should not report error") 41 | } 42 | continue 43 | } 44 | if err != nil { 45 | t.Error(td.rawurl, "non nil URL should not report error") 46 | } 47 | if url.HostPort != td.url.HostPort { 48 | t.Error(td.rawurl, "parsed hostPort wrong:", td.url.HostPort, "got", url.HostPort) 49 | } 50 | if url.Host != td.url.Host { 51 | t.Error(td.rawurl, "parsed host wrong:", td.url.Host, "got", url.Host) 52 | } 53 | if url.Port != td.url.Port { 54 | t.Error(td.rawurl, "parsed port wrong:", td.url.Port, "got", url.Port) 55 | } 56 | if url.Domain != td.url.Domain { 57 | t.Error(td.rawurl, "parsed domain wrong:", td.url.Domain, "got", url.Domain) 58 | } 59 | if url.Path != td.url.Path { 60 | t.Error(td.rawurl, "parsed path wrong:", td.url.Path, "got", url.Path) 61 | } 62 | } 63 | } 64 | 65 | func TestParseHeader(t *testing.T) { 66 | var testData = []struct { 67 | raw string 68 | newraw string 69 | header *Header 70 | }{ 71 | {"content-length: 64\r\nConnection: keep-alive\r\n\r\n", 72 | "content-length: 64\r\n", 73 | &Header{ContLen: 64, Chunking: false, ConnectionKeepAlive: true}}, 74 | {"Connection: keep-alive\r\nKeep-Alive: timeout=10\r\nTransfer-Encoding: chunked\r\nTE: trailers\r\n\r\n", 75 | "", 76 | &Header{ContLen: -1, Chunking: true, ConnectionKeepAlive: true, 77 | KeepAlive: 10 * time.Second}}, 78 | {"Connection:\r\n keep-alive\r\nKeep-Alive: max=5,\r\n timeout=10\r\n\r\n", 79 | "", 80 | &Header{ContLen: -1, Chunking: false, ConnectionKeepAlive: true, 81 | KeepAlive: 10 * time.Second}}, 82 | {"Connection: \r\n close\r\nLong: line\r\n continued\r\n\tagain\r\n\r\n", 83 | "Long: line continued again\r\n", 84 | &Header{ContLen: -1, Chunking: false, ConnectionKeepAlive: false}}, 85 | } 86 | for _, td := range testData { 87 | var h Header 88 | var newraw bytes.Buffer 89 | h.parseHeader(bufio.NewReader(strings.NewReader(td.raw)), &newraw, nil) 90 | if h.ContLen != td.header.ContLen { 91 | t.Errorf("%q parsed content length wrong, should be %d, get %d\n", 92 | td.raw, td.header.ContLen, h.ContLen) 93 | } 94 | if h.Chunking != td.header.Chunking { 95 | t.Errorf("%q parsed chunking wrong, should be %t, get %t\n", 96 | td.raw, td.header.Chunking, h.Chunking) 97 | } 98 | if h.ConnectionKeepAlive != td.header.ConnectionKeepAlive { 99 | t.Errorf("%q parsed connection wrong, should be %v, get %v\n", 100 | td.raw, td.header.ConnectionKeepAlive, h.ConnectionKeepAlive) 101 | } 102 | if h.KeepAlive != td.header.KeepAlive { 103 | t.Errorf("%q parsed keep alive wrong, should be %v, get %v\n", 104 | td.raw, td.header.KeepAlive, h.KeepAlive) 105 | } 106 | if newraw.String() != td.newraw { 107 | t.Errorf("%q parsed raw wrong\nshould be: %q\ngot: %q\n", 108 | td.raw, td.newraw, newraw.Bytes()) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /install-cow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=0.9.8 4 | 5 | arch=`uname -m` 6 | case $arch in 7 | "x86_64") 8 | arch="64" 9 | ;; 10 | "i386" | "i586" | "i486" | "i686") 11 | arch="32" 12 | ;; 13 | "armv5tel" | "armv6l" | "armv7l") 14 | features=`cat /proc/cpuinfo | grep Features` 15 | if [[ ! "$features" =~ "vfp" ]]; then 16 | #arm without vfp must use GOARM=5 binary 17 | #see https://github.com/golang/go/wiki/GoArm 18 | arch="-armv5tel" 19 | else 20 | arch="-$arch" 21 | fi 22 | ;; 23 | *) 24 | echo "$arch currently has no precompiled binary" 25 | ;; 26 | esac 27 | 28 | os=`uname -s` 29 | case $os in 30 | "Darwin") 31 | os="mac" 32 | ;; 33 | "Linux") 34 | os="linux" 35 | ;; 36 | *) 37 | echo "$os currently has no precompiled binary" 38 | exit 1 39 | esac 40 | 41 | exit_on_fail() { 42 | if [ $? != 0 ]; then 43 | echo $1 44 | exit 1 45 | fi 46 | } 47 | 48 | while true; do 49 | # Get install directory from environment variable. 50 | if [[ -n $COW_INSTALLDIR && -d $COW_INSTALLDIR ]]; then 51 | install_dir=$COW_INSTALLDIR 52 | break 53 | fi 54 | 55 | # Get installation directory from user 56 | echo -n "Install cow binary to which directory (absolute path, defaults to current dir): " 57 | read install_dir $la_dir/$plist || \ 126 | exit_on_fail "Download startup plist file to $la_dir failed" 127 | fi 128 | 129 | # Move binary to install directory 130 | echo "Move $tmpbin to $install_dir (will run sudo if no write permission to install directory)" 131 | if [ -w $install_dir ]; then 132 | mv $tmpbin $install_dir 133 | else 134 | sudo mv $tmpbin $install_dir 135 | fi 136 | exit_on_fail "Failed to move $tmpbin to $install_dir" 137 | rmdir $tmpdir 138 | 139 | # Done 140 | echo 141 | if $is_update; then 142 | echo "Update finished." 143 | else 144 | echo "Installation finished." 145 | echo "Please edit $config_dir/rc according to your own settings." 146 | echo 'After that, execute "cow &" to start cow and run in background.' 147 | fi 148 | 149 | -------------------------------------------------------------------------------- /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", false, "request log") 46 | flag.BoolVar((*bool)(&dbgRep), "reply", false, "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 | // "flag" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | // "runtime/pprof" 9 | "sync" 10 | "syscall" 11 | ) 12 | 13 | // var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") 14 | var ( 15 | quit chan struct{} 16 | relaunch bool 17 | ) 18 | 19 | // This code is from goagain 20 | func lookPath() (argv0 string, err error) { 21 | argv0, err = exec.LookPath(os.Args[0]) 22 | if nil != err { 23 | return 24 | } 25 | if _, err = os.Stat(argv0); nil != err { 26 | return 27 | } 28 | return 29 | } 30 | 31 | func main() { 32 | quit = make(chan struct{}) 33 | // Parse flags after load config to allow override options in config 34 | cmdLineConfig := parseCmdLineConfig() 35 | if cmdLineConfig.PrintVer { 36 | printVersion() 37 | os.Exit(0) 38 | } 39 | 40 | parseConfig(cmdLineConfig.RcFile, cmdLineConfig) 41 | 42 | initSelfListenAddr() 43 | initLog() 44 | initAuth() 45 | initSiteStat() 46 | initPAC() // initPAC uses siteStat, so must init after site stat 47 | 48 | initStat() 49 | 50 | initParentPool() 51 | 52 | /* 53 | if *cpuprofile != "" { 54 | f, err := os.Create(*cpuprofile) 55 | if err != nil { 56 | Fatal(err) 57 | } 58 | pprof.StartCPUProfile(f) 59 | } 60 | */ 61 | 62 | if config.Core > 0 { 63 | runtime.GOMAXPROCS(config.Core) 64 | } 65 | 66 | go sigHandler() 67 | go runSSH() 68 | if config.EstimateTimeout { 69 | go runEstimateTimeout() 70 | } else { 71 | info.Println("timeout estimation disabled") 72 | } 73 | 74 | var wg sync.WaitGroup 75 | wg.Add(len(listenProxy)) 76 | for _, proxy := range listenProxy { 77 | go proxy.Serve(&wg, quit) 78 | } 79 | 80 | wg.Wait() 81 | 82 | if relaunch { 83 | info.Println("Relunching cow...") 84 | // Need to fork me. 85 | argv0, err := lookPath() 86 | if nil != err { 87 | errl.Println(err) 88 | return 89 | } 90 | 91 | err = syscall.Exec(argv0, os.Args, os.Environ()) 92 | if err != nil { 93 | errl.Println(err) 94 | } 95 | } 96 | debug.Println("the main process is , exiting...") 97 | } 98 | -------------------------------------------------------------------------------- /main_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func sigHandler() { 12 | sigChan := make(chan os.Signal, 1) 13 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) 14 | 15 | for sig := range sigChan { 16 | // May handle other signals in the future. 17 | info.Printf("%v caught, exit\n", sig) 18 | storeSiteStat(siteStatExit) 19 | if sig == syscall.SIGUSR1 { 20 | relaunch = true 21 | } 22 | close(quit) 23 | break 24 | } 25 | /* 26 | if *cpuprofile != "" { 27 | pprof.StopCPUProfile() 28 | } 29 | */ 30 | } 31 | -------------------------------------------------------------------------------- /main_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | ) 8 | 9 | func sigHandler() { 10 | // TODO On Windows, these signals will not be triggered on closing cmd 11 | // window. How to detect this? 12 | sigChan := make(chan os.Signal, 1) 13 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 14 | 15 | for sig := range sigChan { 16 | // May handle other signals in the future. 17 | info.Printf("%v caught, exit\n", sig) 18 | storeSiteStat(siteStatExit) 19 | // Windows has no SIGUSR1 signal, so relaunching is not supported now. 20 | /* 21 | if sig == syscall.SIGUSR1 { 22 | relaunch = true 23 | } 24 | */ 25 | close(quit) 26 | break 27 | } 28 | /* 29 | if *cpuprofile != "" { 30 | pprof.StopCPUProfile() 31 | } 32 | */ 33 | } 34 | -------------------------------------------------------------------------------- /pac.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "sync" 9 | "text/template" 10 | "time" 11 | ) 12 | 13 | var pac struct { 14 | template *template.Template 15 | topLevelDomain string 16 | directList string 17 | // Assignments and reads to directList are in different goroutines. Go 18 | // does not guarantee atomic assignment, so we should protect these racing 19 | // access. 20 | dLRWMutex sync.RWMutex 21 | } 22 | 23 | func getDirectList() string { 24 | pac.dLRWMutex.RLock() 25 | dl := pac.directList 26 | pac.dLRWMutex.RUnlock() 27 | return dl 28 | } 29 | 30 | func updateDirectList() { 31 | dl := strings.Join(siteStat.GetDirectList(), "\",\n\"") 32 | pac.dLRWMutex.Lock() 33 | pac.directList = dl 34 | pac.dLRWMutex.Unlock() 35 | } 36 | 37 | func init() { 38 | const pacRawTmpl = `var direct = 'DIRECT'; 39 | var httpProxy = 'PROXY {{.ProxyAddr}}; DIRECT'; 40 | 41 | var directList = [ 42 | "", 43 | "{{.DirectDomains}}" 44 | ]; 45 | 46 | var directAcc = {}; 47 | for (var i = 0; i < directList.length; i += 1) { 48 | directAcc[directList[i]] = true; 49 | } 50 | 51 | var topLevel = { 52 | {{.TopLevel}} 53 | }; 54 | 55 | // hostIsIP determines whether a host address is an IP address and whether 56 | // it is private. Currenly only handles IPv4 addresses. 57 | function hostIsIP(host) { 58 | var part = host.split('.'); 59 | if (part.length != 4) { 60 | return [false, false]; 61 | } 62 | var n; 63 | for (var i = 3; i >= 0; i--) { 64 | if (part[i].length === 0 || part[i].length > 3) { 65 | return [false, false]; 66 | } 67 | n = Number(part[i]); 68 | if (isNaN(n) || n < 0 || n > 255) { 69 | return [false, false]; 70 | } 71 | } 72 | if (part[0] == '127' || part[0] == '10' || (part[0] == '192' && part[1] == '168')) { 73 | return [true, true]; 74 | } 75 | if (part[0] == '172') { 76 | n = Number(part[1]); 77 | if (16 <= n && n <= 31) { 78 | return [true, true]; 79 | } 80 | } 81 | return [true, false]; 82 | } 83 | 84 | function host2Domain(host) { 85 | var arr, isIP, isPrivate; 86 | arr = hostIsIP(host); 87 | isIP = arr[0]; 88 | isPrivate = arr[1]; 89 | if (isPrivate) { 90 | return ""; 91 | } 92 | if (isIP) { 93 | return host; 94 | } 95 | 96 | var lastDot = host.lastIndexOf('.'); 97 | if (lastDot === -1) { 98 | return ""; // simple host name has no domain 99 | } 100 | // Find the second last dot 101 | dot2ndLast = host.lastIndexOf(".", lastDot-1); 102 | if (dot2ndLast === -1) 103 | return host; 104 | 105 | var part = host.substring(dot2ndLast+1, lastDot); 106 | if (topLevel[part]) { 107 | var dot3rdLast = host.lastIndexOf(".", dot2ndLast-1); 108 | if (dot3rdLast === -1) { 109 | return host; 110 | } 111 | return host.substring(dot3rdLast+1); 112 | } 113 | return host.substring(dot2ndLast+1); 114 | } 115 | 116 | function FindProxyForURL(url, host) { 117 | if (url.substring(0,4) == "ftp:") 118 | return direct; 119 | if (host.substring(0,7) == "::ffff:") 120 | return direct; 121 | if (host.indexOf(".local", host.length - 6) !== -1) { 122 | return direct; 123 | } 124 | var domain = host2Domain(host); 125 | if (host.length == domain.length) { 126 | return directAcc[host] ? direct : httpProxy; 127 | } 128 | return (directAcc[host] || directAcc[domain]) ? direct : httpProxy; 129 | } 130 | ` 131 | var err error 132 | pac.template, err = template.New("pac").Parse(pacRawTmpl) 133 | if err != nil { 134 | Fatal("Internal error on generating pac file template:", err) 135 | } 136 | 137 | var buf bytes.Buffer 138 | for k, _ := range topLevelDomain { 139 | buf.WriteString(fmt.Sprintf("\t\"%s\": true,\n", k)) 140 | } 141 | pac.topLevelDomain = buf.String()[:buf.Len()-2] // remove the final comma 142 | } 143 | 144 | // No need for content-length as we are closing connection 145 | var pacHeader = []byte("HTTP/1.1 200 OK\r\nServer: cow-proxy\r\n" + 146 | "Content-Type: application/x-ns-proxy-autoconfig\r\nConnection: close\r\n\r\n") 147 | 148 | // Different client will have different proxy URL, so generate it upon each request. 149 | func genPAC(c *clientConn) []byte { 150 | buf := new(bytes.Buffer) 151 | 152 | hproxy, ok := c.proxy.(*httpProxy) 153 | if !ok { 154 | panic("sendPAC should only be called for http proxy") 155 | } 156 | 157 | proxyAddr := hproxy.addrInPAC 158 | if proxyAddr == "" { 159 | host, _, err := net.SplitHostPort(c.LocalAddr().String()) 160 | // This is the only check to split host port on tcp addr's string 161 | // representation in COW. Keep it so we will notice if there's any 162 | // problem in the future. 163 | if err != nil { 164 | panic("split host port on local address error") 165 | } 166 | proxyAddr = net.JoinHostPort(host, hproxy.port) 167 | } 168 | 169 | dl := getDirectList() 170 | 171 | if dl == "" { 172 | // Empty direct domain list 173 | buf.Write(pacHeader) 174 | pacproxy := fmt.Sprintf("function FindProxyForURL(url, host) { return 'PROXY %s; DIRECT'; };", 175 | proxyAddr) 176 | buf.Write([]byte(pacproxy)) 177 | return buf.Bytes() 178 | } 179 | 180 | data := struct { 181 | ProxyAddr string 182 | DirectDomains string 183 | TopLevel string 184 | }{ 185 | proxyAddr, 186 | dl, 187 | pac.topLevelDomain, 188 | } 189 | 190 | buf.Write(pacHeader) 191 | if err := pac.template.Execute(buf, data); err != nil { 192 | errl.Println("Error generating pac file:", err) 193 | panic("Error generating pac file") 194 | } 195 | return buf.Bytes() 196 | } 197 | 198 | func initPAC() { 199 | // we can't control goroutine scheduling, make sure when 200 | // initPAC is done, direct list is updated 201 | updateDirectList() 202 | go func() { 203 | for { 204 | time.Sleep(time.Minute) 205 | updateDirectList() 206 | } 207 | }() 208 | } 209 | 210 | func sendPAC(c *clientConn) error { 211 | _, err := c.Write(genPAC(c)) 212 | if err != nil { 213 | debug.Printf("cli(%s) error sending PAC: %s", c.RemoteAddr(), err) 214 | } 215 | return err 216 | } 217 | -------------------------------------------------------------------------------- /pac.js: -------------------------------------------------------------------------------- 1 | var direct = 'DIRECT'; 2 | var httpProxy = 'PROXY'; 3 | 4 | var directList = [ 5 | "", // corresponds to simple host name and ip address 6 | "taobao.com", 7 | "www.baidu.com" 8 | ]; 9 | 10 | var directAcc = {}; 11 | for (var i = 0; i < directList.length; i += 1) { 12 | directAcc[directList[i]] = true; 13 | } 14 | 15 | var topLevel = { 16 | "ac": true, 17 | "co": true, 18 | "com": true, 19 | "edu": true, 20 | "gov": true, 21 | "net": true, 22 | "org": true 23 | }; 24 | 25 | // hostIsIP determines whether a host address is an IP address and whether 26 | // it is private. Currenly only handles IPv4 addresses. 27 | function hostIsIP(host) { 28 | var part = host.split('.'); 29 | if (part.length != 4) { 30 | return [false, false]; 31 | } 32 | var n; 33 | for (var i = 3; i >= 0; i--) { 34 | if (part[i].length === 0 || part[i].length > 3) { 35 | return [false, false]; 36 | } 37 | n = Number(part[i]); 38 | if (isNaN(n) || n < 0 || n > 255) { 39 | return [false, false]; 40 | } 41 | } 42 | if (part[0] == '127' || part[0] == '10' || (part[0] == '192' && part[1] == '168')) { 43 | return [true, true]; 44 | } 45 | if (part[0] == '172') { 46 | n = Number(part[1]); 47 | if (16 <= n && n <= 31) { 48 | return [true, true]; 49 | } 50 | } 51 | return [true, false]; 52 | } 53 | 54 | function host2Domain(host) { 55 | var arr, isIP, isPrivate; 56 | arr = hostIsIP(host); 57 | isIP = arr[0]; 58 | isPrivate = arr[1]; 59 | if (isPrivate) { 60 | return ""; 61 | } 62 | if (isIP) { 63 | return host; 64 | } 65 | 66 | var lastDot = host.lastIndexOf('.'); 67 | if (lastDot === -1) { 68 | return ""; // simple host name has no domain 69 | } 70 | // Find the second last dot 71 | dot2ndLast = host.lastIndexOf(".", lastDot-1); 72 | if (dot2ndLast === -1) 73 | return host; 74 | 75 | var part = host.substring(dot2ndLast+1, lastDot); 76 | if (topLevel[part]) { 77 | var dot3rdLast = host.lastIndexOf(".", dot2ndLast-1); 78 | if (dot3rdLast === -1) { 79 | return host; 80 | } 81 | return host.substring(dot3rdLast+1); 82 | } 83 | return host.substring(dot2ndLast+1); 84 | } 85 | 86 | function FindProxyForURL(url, host) { 87 | if (url.substring(0,4) == "ftp:") 88 | return direct; 89 | if (host.indexOf(".local", host.length - 6) !== -1) { 90 | return direct; 91 | } 92 | var domain = host2Domain(host); 93 | if (host.length == domain.length) { 94 | return directAcc[host] ? direct : httpProxy; 95 | } 96 | return (directAcc[host] || directAcc[domain]) ? direct : httpProxy; 97 | } 98 | 99 | // Tests 100 | 101 | var testData, td, i; 102 | 103 | testData = [ 104 | { ip: '127.0.0.1', isIP: true, isPrivate: true }, 105 | { ip: '127.2.1.1', isIP: true, isPrivate: true }, 106 | { ip: '192.168.1.1', isIP: true, isPrivate: true }, 107 | { ip: '172.16.1.1', isIP: true, isPrivate: true }, 108 | { ip: '172.20.1.1', isIP: true, isPrivate: true }, 109 | { ip: '172.31.1.1', isIP: true, isPrivate: true }, 110 | { ip: '172.15.1.1', isIP: true, isPrivate: false }, 111 | { ip: '172.32.1.1', isIP: true, isPrivate: false }, 112 | { ip: '10.16.1.1', isIP: true, isPrivate: true }, 113 | { ip: '12.3.4.5', isIP: true, isPrivate: false }, 114 | { ip: '1.2.3.4.5', isIP: false, isPrivate: false }, 115 | { ip: 'google.com', isIP: false, isPrivate: false }, 116 | { ip: 'www.google.com.hk', isIP: false, isPrivate: false } 117 | ]; 118 | 119 | for (i = 0; i < testData.length; i += 1) { 120 | td = testData[i]; 121 | arr = hostIsIP(td.ip); 122 | if (arr[0] !== td.isIP) { 123 | if (td.isIP) { 124 | console.log(td.ip + " is ip"); 125 | } else { 126 | console.log(td.ip + " is NOT ip"); 127 | } 128 | } 129 | if (arr[0] !== td.isIP) { 130 | if (td.isIP) { 131 | console.log(td.ip + " is private ip"); 132 | } else { 133 | console.log(td.ip + " is NOT private ip"); 134 | } 135 | } 136 | } 137 | 138 | testData = [ 139 | // private ip should return direct 140 | { host: '192.168.1.1', mode: direct}, 141 | { host: '10.1.1.1', mode: direct}, 142 | { host: '172.16.2.1', mode: direct}, 143 | { host: '172.20.255.255', mode: direct}, 144 | { host: '172.31.255.255', mode: direct}, 145 | { host: '192.168.2.255', mode: direct}, 146 | 147 | // simple host should return direct 148 | { host: 'localhost', mode: direct}, 149 | { host: 'simple', mode: direct}, 150 | 151 | // non private ip should return proxy 152 | { host: '172.32.2.255', mode: httpProxy}, 153 | { host: '172.15.0.255', mode: httpProxy}, 154 | { host: '12.20.2.1', mode: httpProxy}, 155 | 156 | // host in direct domain/host should return direct 157 | { host: 'taobao.com', mode: direct}, 158 | { host: 'www.taobao.com', mode: direct}, 159 | { host: 'www.baidu.com', mode: direct}, 160 | 161 | // host not in direct domain should return proxy 162 | { host: 'baidu.com', mode: httpProxy}, 163 | { host: 'foo.baidu.com', mode: httpProxy}, 164 | { host: 'google.com', mode: httpProxy}, 165 | { host: 'www.google.com', mode: httpProxy}, 166 | { host: 'www.google.com.hk', mode: httpProxy}, 167 | 168 | // host in local domain should return direct 169 | { host: 'test.local', mode: direct}, 170 | { host: '.local', mode: direct}, 171 | ]; 172 | 173 | for (i = 0; i < testData.length; i += 1) { 174 | td = testData[i]; 175 | if (FindProxyForURL("", td.host) !== td.mode) { 176 | if (td.mode === direct) { 177 | console.log(td.host + " should return direct"); 178 | } else { 179 | console.log(td.host + " should return proxy"); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /parent_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" 9 | "hash/crc32" 10 | "io" 11 | "math/rand" 12 | "net" 13 | "sort" 14 | "strconv" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // Interface that all types of parent proxies should support. 20 | type ParentProxy interface { 21 | connect(*URL) (net.Conn, error) 22 | getServer() string // for use in updating server latency 23 | genConfig() string // for upgrading config 24 | } 25 | 26 | // Interface for different proxy selection strategy. 27 | type ParentPool interface { 28 | add(ParentProxy) 29 | empty() bool 30 | // Select a proxy from the pool and connect. May try several proxies until 31 | // one that succees, return nil and error if all parent proxies fail. 32 | connect(*URL) (net.Conn, error) 33 | } 34 | 35 | // Init parentProxy to be backup pool. So config parsing have a pool to add 36 | // parent proxies. 37 | var parentProxy ParentPool = &backupParentPool{} 38 | 39 | func initParentPool() { 40 | backPool, ok := parentProxy.(*backupParentPool) 41 | if !ok { 42 | panic("initial parent pool should be backup pool") 43 | } 44 | if debug { 45 | printParentProxy(backPool.parent) 46 | } 47 | if len(backPool.parent) == 0 { 48 | info.Println("no parent proxy server") 49 | return 50 | } 51 | if len(backPool.parent) == 1 && config.LoadBalance != loadBalanceBackup { 52 | debug.Println("only 1 parent, no need for load balance") 53 | config.LoadBalance = loadBalanceBackup 54 | } 55 | 56 | switch config.LoadBalance { 57 | case loadBalanceHash: 58 | debug.Println("hash parent pool", len(backPool.parent)) 59 | parentProxy = &hashParentPool{*backPool} 60 | case loadBalanceLatency: 61 | debug.Println("latency parent pool", len(backPool.parent)) 62 | go updateParentProxyLatency() 63 | parentProxy = newLatencyParentPool(backPool.parent) 64 | } 65 | } 66 | 67 | func printParentProxy(parent []ParentWithFail) { 68 | debug.Println("avaiable parent proxies:") 69 | for _, pp := range parent { 70 | switch pc := pp.ParentProxy.(type) { 71 | case *shadowsocksParent: 72 | debug.Println("\tshadowsocks: ", pc.server) 73 | case *httpParent: 74 | debug.Println("\thttp parent: ", pc.server) 75 | case *socksParent: 76 | debug.Println("\tsocks parent: ", pc.server) 77 | case *cowParent: 78 | debug.Println("\tcow parent: ", pc.server) 79 | } 80 | } 81 | } 82 | 83 | type ParentWithFail struct { 84 | ParentProxy 85 | fail int 86 | } 87 | 88 | // Backup load balance strategy: 89 | // Select proxy in the order they appear in config. 90 | type backupParentPool struct { 91 | parent []ParentWithFail 92 | } 93 | 94 | func (pp *backupParentPool) empty() bool { 95 | return len(pp.parent) == 0 96 | } 97 | 98 | func (pp *backupParentPool) add(parent ParentProxy) { 99 | pp.parent = append(pp.parent, ParentWithFail{parent, 0}) 100 | } 101 | 102 | func (pp *backupParentPool) connect(url *URL) (srvconn net.Conn, err error) { 103 | return connectInOrder(url, pp.parent, 0) 104 | } 105 | 106 | // Hash load balance strategy: 107 | // Each host will use a proxy based on a hash value. 108 | type hashParentPool struct { 109 | backupParentPool 110 | } 111 | 112 | func (pp *hashParentPool) connect(url *URL) (srvconn net.Conn, err error) { 113 | start := int(crc32.ChecksumIEEE([]byte(url.Host)) % uint32(len(pp.parent))) 114 | debug.Printf("hash host %s try %d parent first", url.Host, start) 115 | return connectInOrder(url, pp.parent, start) 116 | } 117 | 118 | func (parent *ParentWithFail) connect(url *URL) (srvconn net.Conn, err error) { 119 | const maxFailCnt = 30 120 | srvconn, err = parent.ParentProxy.connect(url) 121 | if err != nil { 122 | if parent.fail < maxFailCnt && !networkBad() { 123 | parent.fail++ 124 | } 125 | return 126 | } 127 | parent.fail = 0 128 | return 129 | } 130 | 131 | func connectInOrder(url *URL, pp []ParentWithFail, start int) (srvconn net.Conn, err error) { 132 | const baseFailCnt = 9 133 | var skipped []int 134 | nproxy := len(pp) 135 | 136 | if nproxy == 0 { 137 | return nil, errors.New("no parent proxy") 138 | } 139 | 140 | for i := 0; i < nproxy; i++ { 141 | proxyId := (start + i) % nproxy 142 | parent := &pp[proxyId] 143 | // skip failed server, but try it with some probability 144 | if parent.fail > 0 && rand.Intn(parent.fail+baseFailCnt) != 0 { 145 | skipped = append(skipped, proxyId) 146 | continue 147 | } 148 | if srvconn, err = parent.connect(url); err == nil { 149 | return 150 | } 151 | } 152 | // last resort, try skipped one, not likely to succeed 153 | for _, skippedId := range skipped { 154 | if srvconn, err = pp[skippedId].connect(url); err == nil { 155 | return 156 | } 157 | } 158 | return nil, err 159 | } 160 | 161 | type ParentWithLatency struct { 162 | ParentProxy 163 | latency time.Duration 164 | } 165 | 166 | type latencyParentPool struct { 167 | parent []ParentWithLatency 168 | } 169 | 170 | func newLatencyParentPool(parent []ParentWithFail) *latencyParentPool { 171 | lp := &latencyParentPool{} 172 | for _, p := range parent { 173 | lp.add(p.ParentProxy) 174 | } 175 | return lp 176 | } 177 | 178 | func (pp *latencyParentPool) empty() bool { 179 | return len(pp.parent) == 0 180 | } 181 | 182 | func (pp *latencyParentPool) add(parent ParentProxy) { 183 | pp.parent = append(pp.parent, ParentWithLatency{parent, 0}) 184 | } 185 | 186 | // Sort interface. 187 | func (pp *latencyParentPool) Len() int { 188 | return len(pp.parent) 189 | } 190 | 191 | func (pp *latencyParentPool) Swap(i, j int) { 192 | p := pp.parent 193 | p[i], p[j] = p[j], p[i] 194 | } 195 | 196 | func (pp *latencyParentPool) Less(i, j int) bool { 197 | p := pp.parent 198 | return p[i].latency < p[j].latency 199 | } 200 | 201 | const latencyMax = time.Hour 202 | 203 | var latencyMutex sync.RWMutex 204 | 205 | func (pp *latencyParentPool) connect(url *URL) (srvconn net.Conn, err error) { 206 | var lp []ParentWithLatency 207 | // Read slice first. 208 | latencyMutex.RLock() 209 | lp = pp.parent 210 | latencyMutex.RUnlock() 211 | 212 | var skipped []int 213 | nproxy := len(lp) 214 | if nproxy == 0 { 215 | return nil, errors.New("no parent proxy") 216 | } 217 | 218 | for i := 0; i < nproxy; i++ { 219 | parent := lp[i] 220 | if parent.latency >= latencyMax { 221 | skipped = append(skipped, i) 222 | continue 223 | } 224 | if srvconn, err = parent.connect(url); err == nil { 225 | debug.Println("lowest latency proxy", parent.getServer()) 226 | return 227 | } 228 | parent.latency = latencyMax 229 | } 230 | // last resort, try skipped one, not likely to succeed 231 | for _, skippedId := range skipped { 232 | if srvconn, err = lp[skippedId].connect(url); err == nil { 233 | return 234 | } 235 | } 236 | return nil, err 237 | } 238 | 239 | func (parent *ParentWithLatency) updateLatency(wg *sync.WaitGroup) { 240 | defer wg.Done() 241 | proxy := parent.ParentProxy 242 | server := proxy.getServer() 243 | 244 | host, port, err := net.SplitHostPort(server) 245 | if err != nil { 246 | panic("split host port parent server error" + err.Error()) 247 | } 248 | 249 | // Resolve host name first, so latency does not include resolve time. 250 | ip, err := net.LookupHost(host) 251 | if err != nil { 252 | parent.latency = latencyMax 253 | return 254 | } 255 | ipPort := net.JoinHostPort(ip[0], port) 256 | 257 | const N = 3 258 | var total time.Duration 259 | for i := 0; i < N; i++ { 260 | now := time.Now() 261 | cn, err := net.DialTimeout("tcp", ipPort, dialTimeout) 262 | if err != nil { 263 | debug.Println("latency update dial:", err) 264 | total += time.Minute // 1 minute as penalty 265 | continue 266 | } 267 | total += time.Now().Sub(now) 268 | cn.Close() 269 | 270 | time.Sleep(5 * time.Millisecond) 271 | } 272 | parent.latency = total / N 273 | debug.Println("latency", server, parent.latency) 274 | } 275 | 276 | func (pp *latencyParentPool) updateLatency() { 277 | // Create a copy, update latency for the copy. 278 | var cp latencyParentPool 279 | cp.parent = append(cp.parent, pp.parent...) 280 | 281 | // cp.parent is value instead of pointer, if we use `_, p := range cp.parent`, 282 | // the value in cp.parent will not be updated. 283 | var wg sync.WaitGroup 284 | wg.Add(len(cp.parent)) 285 | for i, _ := range cp.parent { 286 | cp.parent[i].updateLatency(&wg) 287 | } 288 | wg.Wait() 289 | 290 | // Sort according to latency. 291 | sort.Stable(&cp) 292 | debug.Println("latency lowest proxy", cp.parent[0].getServer()) 293 | 294 | // Update parent slice. 295 | latencyMutex.Lock() 296 | pp.parent = cp.parent 297 | latencyMutex.Unlock() 298 | } 299 | 300 | func updateParentProxyLatency() { 301 | lp, ok := parentProxy.(*latencyParentPool) 302 | if !ok { 303 | return 304 | } 305 | 306 | for { 307 | lp.updateLatency() 308 | time.Sleep(60 * time.Second) 309 | } 310 | } 311 | 312 | // http parent proxy 313 | type httpParent struct { 314 | server string 315 | userPasswd string // for upgrade config 316 | authHeader []byte 317 | } 318 | 319 | type httpConn struct { 320 | net.Conn 321 | parent *httpParent 322 | } 323 | 324 | func (s httpConn) String() string { 325 | return "http parent proxy " + s.parent.server 326 | } 327 | 328 | func newHttpParent(server string) *httpParent { 329 | return &httpParent{server: server} 330 | } 331 | 332 | func (hp *httpParent) getServer() string { 333 | return hp.server 334 | } 335 | 336 | func (hp *httpParent) genConfig() string { 337 | if hp.userPasswd != "" { 338 | return fmt.Sprintf("proxy = http://%s@%s", hp.userPasswd, hp.server) 339 | } else { 340 | return fmt.Sprintf("proxy = http://%s", hp.server) 341 | } 342 | } 343 | 344 | func (hp *httpParent) initAuth(userPasswd string) { 345 | if userPasswd == "" { 346 | return 347 | } 348 | hp.userPasswd = userPasswd 349 | b64 := base64.StdEncoding.EncodeToString([]byte(userPasswd)) 350 | hp.authHeader = []byte(headerProxyAuthorization + ": Basic " + b64 + CRLF) 351 | } 352 | 353 | func (hp *httpParent) connect(url *URL) (net.Conn, error) { 354 | c, err := net.Dial("tcp", hp.server) 355 | if err != nil { 356 | errl.Printf("can't connect to http parent %s for %s: %v\n", 357 | hp.server, url.HostPort, err) 358 | return nil, err 359 | } 360 | debug.Printf("connected to: %s via http parent: %s\n", 361 | url.HostPort, hp.server) 362 | return httpConn{c, hp}, nil 363 | } 364 | 365 | // shadowsocks parent proxy 366 | type shadowsocksParent struct { 367 | server string 368 | method string // method and passwd are for upgrade config 369 | passwd string 370 | cipher *ss.Cipher 371 | } 372 | 373 | type shadowsocksConn struct { 374 | net.Conn 375 | parent *shadowsocksParent 376 | } 377 | 378 | func (s shadowsocksConn) String() string { 379 | return "shadowsocks proxy " + s.parent.server 380 | } 381 | 382 | // In order to use parent proxy in the order specified in the config file, we 383 | // insert an uninitialized proxy into parent proxy list, and initialize it 384 | // when all its config have been parsed. 385 | 386 | func newShadowsocksParent(server string) *shadowsocksParent { 387 | return &shadowsocksParent{server: server} 388 | } 389 | 390 | func (sp *shadowsocksParent) getServer() string { 391 | return sp.server 392 | } 393 | 394 | func (sp *shadowsocksParent) genConfig() string { 395 | method := sp.method 396 | if method == "" { 397 | method = "table" 398 | } 399 | return fmt.Sprintf("proxy = ss://%s:%s@%s", method, sp.passwd, sp.server) 400 | } 401 | 402 | func (sp *shadowsocksParent) initCipher(method, passwd string) { 403 | sp.method = method 404 | sp.passwd = passwd 405 | cipher, err := ss.NewCipher(method, passwd) 406 | if err != nil { 407 | Fatal("create shadowsocks cipher:", err) 408 | } 409 | sp.cipher = cipher 410 | } 411 | 412 | func (sp *shadowsocksParent) connect(url *URL) (net.Conn, error) { 413 | c, err := ss.Dial(url.HostPort, sp.server, sp.cipher.Copy()) 414 | if err != nil { 415 | errl.Printf("can't connect to shadowsocks parent %s for %s: %v\n", 416 | sp.server, url.HostPort, err) 417 | return nil, err 418 | } 419 | debug.Println("connected to:", url.HostPort, "via shadowsocks:", sp.server) 420 | return shadowsocksConn{c, sp}, nil 421 | } 422 | 423 | // cow parent proxy 424 | type cowParent struct { 425 | server string 426 | method string 427 | passwd string 428 | cipher *ss.Cipher 429 | } 430 | 431 | type cowConn struct { 432 | net.Conn 433 | parent *cowParent 434 | } 435 | 436 | func (s cowConn) String() string { 437 | return "cow proxy " + s.parent.server 438 | } 439 | 440 | func newCowParent(srv, method, passwd string) *cowParent { 441 | cipher, err := ss.NewCipher(method, passwd) 442 | if err != nil { 443 | Fatal("create cow cipher:", err) 444 | } 445 | return &cowParent{srv, method, passwd, cipher} 446 | } 447 | 448 | func (cp *cowParent) getServer() string { 449 | return cp.server 450 | } 451 | 452 | func (cp *cowParent) genConfig() string { 453 | method := cp.method 454 | if method == "" { 455 | method = "table" 456 | } 457 | return fmt.Sprintf("proxy = cow://%s:%s@%s", method, cp.passwd, cp.server) 458 | } 459 | 460 | func (cp *cowParent) connect(url *URL) (net.Conn, error) { 461 | c, err := net.Dial("tcp", cp.server) 462 | if err != nil { 463 | errl.Printf("can't connect to cow parent %s for %s: %v\n", 464 | cp.server, url.HostPort, err) 465 | return nil, err 466 | } 467 | debug.Printf("connected to: %s via cow parent: %s\n", 468 | url.HostPort, cp.server) 469 | ssconn := ss.NewConn(c, cp.cipher.Copy()) 470 | return cowConn{ssconn, cp}, nil 471 | } 472 | 473 | // For socks documentation, refer to rfc 1928 http://www.ietf.org/rfc/rfc1928.txt 474 | 475 | var socksError = [...]string{ 476 | 1: "General SOCKS server failure", 477 | 2: "Connection not allowed by ruleset", 478 | 3: "Network unreachable", 479 | 4: "Host unreachable", 480 | 5: "Connection refused", 481 | 6: "TTL expired", 482 | 7: "Command not supported", 483 | 8: "Address type not supported", 484 | 9: "to X'FF' unassigned", 485 | } 486 | 487 | var socksProtocolErr = errors.New("socks protocol error") 488 | 489 | var socksMsgVerMethodSelection = []byte{ 490 | 0x5, // version 5 491 | 1, // n method 492 | 0, // no authorization required 493 | } 494 | 495 | // socks5 parent proxy 496 | type socksParent struct { 497 | server string 498 | } 499 | 500 | type socksConn struct { 501 | net.Conn 502 | parent *socksParent 503 | } 504 | 505 | func (s socksConn) String() string { 506 | return "socks proxy " + s.parent.server 507 | } 508 | 509 | func newSocksParent(server string) *socksParent { 510 | return &socksParent{server} 511 | } 512 | 513 | func (sp *socksParent) getServer() string { 514 | return sp.server 515 | } 516 | 517 | func (sp *socksParent) genConfig() string { 518 | return fmt.Sprintf("proxy = socks5://%s", sp.server) 519 | } 520 | 521 | func (sp *socksParent) connect(url *URL) (net.Conn, error) { 522 | c, err := net.Dial("tcp", sp.server) 523 | if err != nil { 524 | errl.Printf("can't connect to socks parent %s for %s: %v\n", 525 | sp.server, url.HostPort, err) 526 | return nil, err 527 | } 528 | hasErr := false 529 | defer func() { 530 | if hasErr { 531 | c.Close() 532 | } 533 | }() 534 | 535 | var n int 536 | if n, err = c.Write(socksMsgVerMethodSelection); n != 3 || err != nil { 537 | errl.Printf("sending ver/method selection msg %v n = %v\n", err, n) 538 | hasErr = true 539 | return nil, err 540 | } 541 | 542 | // version/method selection 543 | repBuf := make([]byte, 2) 544 | _, err = io.ReadFull(c, repBuf) 545 | if err != nil { 546 | errl.Printf("read ver/method selection error %v\n", err) 547 | hasErr = true 548 | return nil, err 549 | } 550 | if repBuf[0] != 5 || repBuf[1] != 0 { 551 | errl.Printf("socks ver/method selection reply error ver %d method %d", 552 | repBuf[0], repBuf[1]) 553 | hasErr = true 554 | return nil, err 555 | } 556 | // debug.Println("Socks version selection done") 557 | 558 | // send connect request 559 | host := url.Host 560 | port, err := strconv.Atoi(url.Port) 561 | if err != nil { 562 | errl.Printf("should not happen, port error %v\n", port) 563 | hasErr = true 564 | return nil, err 565 | } 566 | 567 | hostLen := len(host) 568 | bufLen := 5 + hostLen + 2 // last 2 is port 569 | reqBuf := make([]byte, bufLen) 570 | reqBuf[0] = 5 // version 5 571 | reqBuf[1] = 1 // cmd: connect 572 | // reqBuf[2] = 0 // rsv: set to 0 when initializing 573 | reqBuf[3] = 3 // atyp: domain name 574 | reqBuf[4] = byte(hostLen) 575 | copy(reqBuf[5:], host) 576 | binary.BigEndian.PutUint16(reqBuf[5+hostLen:5+hostLen+2], uint16(port)) 577 | 578 | if n, err = c.Write(reqBuf); err != nil || n != bufLen { 579 | errl.Printf("send socks request err %v n %d\n", err, n) 580 | hasErr = true 581 | return nil, err 582 | } 583 | 584 | // I'm not clear why the buffer is fixed at 10. The rfc document does not say this. 585 | // Polipo set this to 10 and I also observed the reply is always 10. 586 | replyBuf := make([]byte, 10) 587 | if n, err = c.Read(replyBuf); err != nil { 588 | // Seems that socks server will close connection if it can't find host 589 | if err != io.EOF { 590 | errl.Printf("read socks reply err %v n %d\n", err, n) 591 | } 592 | hasErr = true 593 | return nil, errors.New("connection failed (by socks server " + sp.server + "). No such host?") 594 | } 595 | // debug.Printf("Socks reply length %d\n", n) 596 | 597 | if replyBuf[0] != 5 { 598 | errl.Printf("socks reply connect %s VER %d not supported\n", url.HostPort, replyBuf[0]) 599 | hasErr = true 600 | return nil, socksProtocolErr 601 | } 602 | if replyBuf[1] != 0 { 603 | errl.Printf("socks reply connect %s error %s\n", url.HostPort, socksError[replyBuf[1]]) 604 | hasErr = true 605 | return nil, socksProtocolErr 606 | } 607 | if replyBuf[3] != 1 { 608 | errl.Printf("socks reply connect %s ATYP %d\n", url.HostPort, replyBuf[3]) 609 | hasErr = true 610 | return nil, socksProtocolErr 611 | } 612 | 613 | debug.Println("connected to:", url.HostPort, "via socks server:", sp.server) 614 | // Now the socket can be used to pass data. 615 | return socksConn{c, sp}, nil 616 | } 617 | -------------------------------------------------------------------------------- /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 | // COW 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:7777", "")} 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:7777"}, 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:7777", ""), 82 | newHttpProxy("127.0.0.1:8888", ""), 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 | "net" 7 | "syscall" 8 | "strings" 9 | ) 10 | 11 | func isErrConnReset(err error) bool { 12 | if ne, ok := err.(*net.OpError); ok { 13 | return strings.Contains(ne.Err.Error(), syscall.ECONNRESET.Error()) 14 | } 15 | return false 16 | } 17 | 18 | func isDNSError(err error) bool { 19 | if _, ok := err.(*net.DNSError); ok { 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | func isErrOpWrite(err error) bool { 26 | ne, ok := err.(*net.OpError) 27 | if !ok { 28 | return false 29 | } 30 | return ne.Op == "write" 31 | } 32 | 33 | func isErrOpRead(err error) bool { 34 | ne, ok := err.(*net.OpError) 35 | if !ok { 36 | return false 37 | } 38 | return ne.Op == "read" 39 | } 40 | 41 | func isErrTooManyOpenFd(err error) bool { 42 | if ne, ok := err.(*net.OpError); ok && (ne.Err == syscall.EMFILE || ne.Err == syscall.ENFILE) { 43 | errl.Println("too many open fd") 44 | return true 45 | } 46 | return false 47 | } 48 | -------------------------------------------------------------------------------- /proxy_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "syscall" 8 | ) 9 | 10 | var _ = fmt.Println 11 | 12 | func isErrConnReset(err error) bool { 13 | // fmt.Printf("calling isErrConnReset for err type: %v Error() %s\n", 14 | // reflect.TypeOf(err), err.Error()) 15 | if ne, ok := err.(*net.OpError); ok { 16 | // fmt.Println("isErrConnReset net.OpError.Err type:", reflect.TypeOf(ne)) 17 | if errno, enok := ne.Err.(syscall.Errno); enok { 18 | // I got these number by print. Only tested on XP. 19 | // fmt.Printf("isErrConnReset errno: %d\n", errno) 20 | return errno == 64 || errno == 10054 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func isDNSError(err error) bool { 27 | /* 28 | fmt.Printf("calling isDNSError for err type: %v %s\n", 29 | reflect.TypeOf(err), err.Error()) 30 | */ 31 | // DNS error are not of type DNSError on Windows, so I used this ugly 32 | // hack. 33 | errMsg := err.Error() 34 | return strings.Contains(errMsg, "No such host") || 35 | strings.Contains(errMsg, "GetAddrInfoW") || 36 | strings.Contains(errMsg, "dial tcp") 37 | } 38 | 39 | func isErrOpWrite(err error) bool { 40 | ne, ok := err.(*net.OpError) 41 | if !ok { 42 | return false 43 | } 44 | return ne.Op == "WSASend" 45 | } 46 | 47 | func isErrOpRead(err error) bool { 48 | ne, ok := err.(*net.OpError) 49 | if !ok { 50 | return false 51 | } 52 | return ne.Op == "WSARecv" 53 | } 54 | 55 | func isErrTooManyOpenFd(err error) bool { 56 | // TODO implement this. 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | # About cow-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 | 7 | # About cow-hide.exe 8 | 9 | Allow you to run COW as a background process, without any notifications. Provided by @xupefei's [cow-hide](https://github.com/xupefei/cow-hide) project. 10 | 11 | Icon from [IconArchive](http://www.iconarchive.com/show/animal-icons-by-martin-berube/cow-icon.html), thanks to the author Martin Berube. 12 | 13 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 4 | 5 | version=`grep '^version=' ./install-cow.sh | sed -s 's/version=//'` 6 | echo "creating cow binary version $version" 7 | 8 | mkdir -p bin 9 | build() { 10 | local name 11 | local goos 12 | local goarch 13 | local goarm 14 | local cgo 15 | local armv 16 | 17 | goos="GOOS=$1" 18 | goarch="GOARCH=$2" 19 | arch=$3 20 | if [[ $2 == "arm" ]]; then 21 | armv=`echo $arch | grep -o [0-9]` 22 | goarm="GOARM=$armv" 23 | fi 24 | 25 | if [[ $1 == "darwin" ]]; then 26 | # Enable CGO for OS X so change network location will not cause problem. 27 | cgo="CGO_ENABLED=1" 28 | else 29 | cgo="CGO_ENABLED=0" 30 | fi 31 | 32 | name=cow-$arch-$version 33 | echo "building $name" 34 | echo $cgo $goos $goarch $goarm go build 35 | eval $cgo $goos $goarch $goarm go build || exit 1 36 | if [[ $1 == "windows" ]]; then 37 | mv cow.exe script 38 | pushd script 39 | sed -e 's/$/\r/' ../doc/sample-config/rc > rc.txt 40 | zip $name.zip cow.exe cow-taskbar.exe cow-hide.exe rc.txt 41 | rm -f cow.exe rc.txt 42 | mv $name.zip ../bin/ 43 | popd 44 | else 45 | mv cow bin/$name 46 | gzip -f bin/$name 47 | fi 48 | } 49 | 50 | build darwin amd64 mac64 51 | #build darwin 386 mac32 52 | build linux amd64 linux64 53 | build linux 386 linux32 54 | build linux arm linux-armv5tel 55 | build linux arm linux-armv6l 56 | build linux arm linux-armv7l 57 | build windows amd64 win64 58 | build windows 386 win32 59 | -------------------------------------------------------------------------------- /script/cow-hide.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfdecyf/cow/41c0fb157c8b939b724ae0d58dad3a1b7cd2e811/script/cow-hide.exe -------------------------------------------------------------------------------- /script/cow-taskbar.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyfdecyf/cow/41c0fb157c8b939b724ae0d58dad3a1b7cd2e811/script/cow-taskbar.exe -------------------------------------------------------------------------------- /script/debugrc: -------------------------------------------------------------------------------- 1 | listen = cow://aes-128-cfb:foobar@127.0.0.1:8899 2 | -------------------------------------------------------------------------------- /script/httprc: -------------------------------------------------------------------------------- 1 | listen = http://127.0.0.1:7788 2 | proxy = cow://aes-128-cfb:foobar@127.0.0.1:8899 3 | alwaysProxy = true 4 | -------------------------------------------------------------------------------- /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/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-cow.sh 15 | sed -i -e "s/当前版本:[^ ]\+ \(.*\)\$/当前版本:$version \1/" README.md 16 | sed -i -e "s/Current version: [^ ]\+ \(.*\)\$/Current version: $version \1/" README-en.md 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 | COW_ADDR=127.0.0.1:8899 12 | 13 | if [[ -z "$TRAVIS" ]]; then 14 | RCDIR=~/.cow/ 15 | else # on travis 16 | RCDIR=./script/ 17 | fi 18 | 19 | ./cow -rc $RCDIR/debugrc -listen=cow://aes-128-cfb:foobar@$COW_ADDR & 20 | parent_pid=$! 21 | ./cow -rc ./script/httprc -listen=http://$PROXY_ADDR & 22 | cow_pid=$! 23 | 24 | stop_cow() { 25 | kill -SIGTERM $parent_pid 26 | kill -SIGTERM $cow_pid 27 | } 28 | trap 'stop_cow' 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 | # get 5 times 45 | for i in {1..2}; do 46 | # -s silent to disable progress meter, but enable --show-error 47 | # -i to include http header 48 | # -L to follow redirect so we should always get HTTP 200 49 | if [[ -n $noproxy ]]; then 50 | cont=`curl -s --show-error -i -L $url 2>&1` 51 | else 52 | cont=`curl -s --show-error -i -L -x $PROXY_ADDR $url 2>&1` 53 | fi 54 | ok=`echo $cont | grep -E -o "HTTP/1\.1 +$code"` 55 | html=`echo $cont | grep -E -o -i "$target"` 56 | if [[ -z $ok || -z $html ]] ; then 57 | echo "==============================" 58 | echo "GET $url FAILED!!!" 59 | echo "$ok" 60 | echo "$html" 61 | echo $cont 62 | echo "==============================" 63 | kill -SIGTERM $cow_pid 64 | exit 1 65 | fi 66 | sleep 0.3 67 | done 68 | echo "passed" 69 | } 70 | 71 | test_get $PROXY_ADDR/pac "apple.com" "noproxy" # test for pac 72 | test_get google.com "" # blocked site, all kinds of block method 76 | test_get https://google.com "" 79 | 80 | # Sites that may timeout on travis. 81 | if [[ -z $TRAVIS ]]; then 82 | test_get plan9.bell-labs.com/magic/man2html/1/2l "" "" "404" # single LF in response header 83 | test_get www.wpxap.com "" # 301 redirect 86 | test_get www.taobao.com "" # chunked encoding, weird can't tests for in script 87 | test_get https://www.alipay.com "" 88 | fi 89 | 90 | stop_cow 91 | sleep 0.5 92 | rm -f ./script/stat* 93 | exit 0 94 | -------------------------------------------------------------------------------- /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-cow.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 cow-proxy "$file" 18 | } 19 | 20 | upload "$version for Linux 32bit" bin/cow-linux32-$version.gz 21 | upload "$version for Linux 64bit" bin/cow-linux64-$version.gz 22 | upload "$version for Windows 64bit" bin/cow-win64-$version.zip 23 | upload "$version for Windows 32bit" bin/cow-win32-$version.zip 24 | upload "$version for OS X 64bit" bin/cow-mac64-$version.gz 25 | -------------------------------------------------------------------------------- /site_blocked.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var blockedDomainList = []string{ 4 | "bit.ly", 5 | "j.mp", 6 | "bitly.com", 7 | "fbcdn.net", 8 | "facebook.com", 9 | "plus.google.com", 10 | "plusone.google.com", 11 | "t.co", 12 | "twimg.com", 13 | "twitpic.com", 14 | "twitter.com", 15 | "youtu.be", 16 | "youtube.com", 17 | "ytimg.com", 18 | } 19 | -------------------------------------------------------------------------------- /site_direct.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var directDomainList = []string{ 4 | // 视频 5 | "xunlei.com", // 迅雷 6 | "kankan.com", 7 | "kanimg.com", 8 | 9 | "tdimg.com", // 土豆 10 | "tudou.com", 11 | "tudouui.com", 12 | 13 | "soku.com", // 优酷 14 | "youku.com", 15 | "ykimg.com", 16 | 17 | "ku6.cn", // 酷六 18 | "ku6.com", 19 | "ku6cdn.com", 20 | "ku6img.com", 21 | 22 | // 电商 23 | "z.cn", 24 | "amazon.cn", 25 | 26 | "360buy.com", 27 | "360buyimg.com", 28 | "jd.com", 29 | 30 | "51buy.com", 31 | "icson.com", 32 | 33 | "dangdang.com", 34 | "ddimg.cn", 35 | 36 | "yihaodian.com", 37 | "yihaodianimg.com", 38 | 39 | "paipai.com", 40 | "paipaiimg.com", 41 | 42 | "tmall.com", 43 | "taobao.com", 44 | "taobaocdn.com", 45 | "tbcdn.cn", 46 | "etao.com", 47 | 48 | "aicdn.com", 49 | "alicdn.com", 50 | "alimama.cn", 51 | "alimama.com", 52 | "alipay.com", 53 | "alipayobjects.com", 54 | 55 | // 银行 56 | "bankcomm.com", 57 | "bankofchina.com", 58 | "95559.com.cn", 59 | "abchina.com", 60 | "95599.cn", 61 | "boc.cn", 62 | "ccb.com", 63 | "cmbchina.com", 64 | "icbc.com.cn", 65 | "spdb.com.cn", 66 | 67 | // 社交 68 | "douban.com", 69 | "t.cn", 70 | "weibo.com", 71 | "zhihu.com", 72 | "kaixin001.com", 73 | "qq.com", 74 | 75 | "renren.com", 76 | "rrimg.com", 77 | "xiaonei.com", 78 | "xnimg.cn", 79 | "xnpic.com", 80 | 81 | "dianping.com", // 点评 82 | "dpfile.com", 83 | 84 | "huaban.com", // 又拍云的几个 85 | "yupoo.com", 86 | "upyun.com", 87 | "upaiyun.com", 88 | 89 | // 新闻门户 90 | "ifanr.cn", 91 | "ifanr.com", 92 | "163.com", 93 | "hexun.com", 94 | "sina.com.cn", 95 | "sinaapp.com", 96 | "sinaimg.cn", 97 | "sinajs.cn", 98 | "sohu.com", 99 | "solidot.org", 100 | 101 | // 搜索 102 | "bing.com", 103 | "bing.com.cn", 104 | "baidu.com", 105 | "bdstatic.com", 106 | "bdimg.com", 107 | "youdao.com", 108 | "sogou.com", 109 | 110 | // Apple 111 | "apple.com", 112 | "apple.com.cn", 113 | "icloud.com", 114 | 115 | // 其他 116 | "macromedia.com", 117 | "mmcdn.cn", 118 | "12306.cn", 119 | } 120 | -------------------------------------------------------------------------------- /sitestat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "math/rand" 9 | "os" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/cyfdecyf/bufio" 15 | ) 16 | 17 | func init() { 18 | rand.Seed(time.Now().Unix()) 19 | } 20 | 21 | // VisitCnt and SiteStat are used to track how many times a site is visited. 22 | // With this information: COW knows which sites are frequently visited, and 23 | // judging whether a site is blocked or not is more reliable. 24 | 25 | const ( 26 | directDelta = 5 27 | blockedDelta = 5 28 | maxCnt = 100 // no protect to update visit cnt, smaller value is unlikely to overflow 29 | userCnt = -1 // this represents user specified host or domain 30 | ) 31 | 32 | type siteVisitMethod int 33 | 34 | // minus operation on visit count may get negative value, so use signed int 35 | type vcntint int8 36 | 37 | type Date time.Time 38 | 39 | const dateLayout = "2006-01-02" 40 | 41 | func (d Date) MarshalJSON() ([]byte, error) { 42 | return []byte("\"" + time.Time(d).Format(dateLayout) + "\""), nil 43 | } 44 | 45 | func (d *Date) UnmarshalJSON(input []byte) error { 46 | if len(input) != len(dateLayout)+2 { 47 | return errors.New(fmt.Sprintf("unmarshaling date: invalid input %s", string(input))) 48 | } 49 | input = input[1 : len(dateLayout)+1] 50 | t, err := time.Parse(dateLayout, string(input)) 51 | *d = Date(t) 52 | return err 53 | } 54 | 55 | // COW don't need very accurate visit count, so update to visit count value is 56 | // not protected. 57 | type VisitCnt struct { 58 | Direct vcntint `json:"direct"` 59 | Blocked vcntint `json:"block"` 60 | Recent Date `json:"recent"` 61 | rUpdated bool // whether Recent is updated, we only need date precision 62 | blockedOn time.Time // when is the site last blocked 63 | } 64 | 65 | func newVisitCnt(direct, blocked vcntint) *VisitCnt { 66 | return &VisitCnt{direct, blocked, Date(time.Now()), true, zeroTime} 67 | } 68 | 69 | func newVisitCntWithTime(direct, blocked vcntint, t time.Time) *VisitCnt { 70 | return &VisitCnt{direct, blocked, Date(t), true, zeroTime} 71 | } 72 | 73 | func (vc *VisitCnt) userSpecified() bool { 74 | return vc.Blocked == userCnt || vc.Direct == userCnt 75 | } 76 | 77 | const siteStaleThreshold = 10 * 24 * time.Hour 78 | 79 | func (vc *VisitCnt) isStale() bool { 80 | return time.Now().Sub(time.Time(vc.Recent)) > siteStaleThreshold 81 | } 82 | 83 | // shouldNotSave returns true if the a VisitCnt is not visited for a long time 84 | // (several days) or is specified by user. 85 | func (vc *VisitCnt) shouldNotSave() bool { 86 | return vc.userSpecified() || vc.isStale() || (vc.Blocked == 0 && vc.Direct == 0) 87 | } 88 | 89 | const tmpBlockedTimeout = 2 * time.Minute 90 | 91 | func (vc *VisitCnt) AsTempBlocked() bool { 92 | return time.Now().Sub(vc.blockedOn) < tmpBlockedTimeout 93 | } 94 | 95 | func (vc *VisitCnt) AsDirect() bool { 96 | return (vc.Blocked == 0) || (vc.Direct-vc.Blocked >= directDelta) || vc.AlwaysDirect() 97 | } 98 | 99 | func (vc *VisitCnt) AsBlocked() bool { 100 | if vc.Blocked == userCnt || vc.AsTempBlocked() { 101 | return true 102 | } 103 | // add some randomness to fix mistake 104 | delta := vc.Blocked - vc.Direct 105 | return delta >= blockedDelta && rand.Intn(int(delta)) != 0 106 | } 107 | 108 | func (vc *VisitCnt) AlwaysDirect() bool { 109 | return vc.Direct == userCnt 110 | } 111 | 112 | func (vc *VisitCnt) AlwaysBlocked() bool { 113 | return vc.Blocked == userCnt 114 | } 115 | 116 | func (vc *VisitCnt) OnceBlocked() bool { 117 | return vc.Blocked > 0 || vc.AlwaysBlocked() || vc.AsTempBlocked() 118 | } 119 | 120 | func (vc *VisitCnt) tempBlocked() { 121 | vc.blockedOn = time.Now() 122 | } 123 | 124 | // time.Time is composed of 3 fields, so need lock to protect update. As 125 | // update of last visit is not frequent (at most once for each domain), use a 126 | // global lock to avoid associating a lock to each VisitCnt. 127 | var visitLock sync.Mutex 128 | 129 | // visit updates visit cnt 130 | func (vc *VisitCnt) visit(inc *vcntint) { 131 | if *inc < maxCnt { 132 | *inc++ 133 | } 134 | // Because of concurrent update, possible for *inc to overflow and become 135 | // negative, but very unlikely. 136 | if *inc > maxCnt || *inc < 0 { 137 | *inc = maxCnt 138 | } 139 | 140 | if !vc.rUpdated { 141 | vc.rUpdated = true 142 | visitLock.Lock() 143 | vc.Recent = Date(time.Now()) 144 | visitLock.Unlock() 145 | } 146 | } 147 | 148 | func (vc *VisitCnt) DirectVisit() { 149 | if networkBad() || vc.userSpecified() { 150 | return 151 | } 152 | // one successful direct visit probably means the site is not actually 153 | // blocked 154 | vc.visit(&vc.Direct) 155 | vc.Blocked = 0 156 | } 157 | 158 | func (vc *VisitCnt) BlockedVisit() { 159 | if networkBad() || vc.userSpecified() { 160 | return 161 | } 162 | // When a site changes from direct to blocked by GFW, COW should learn 163 | // this quickly and remove it from the PAC ASAP. So change direct to 0 164 | // once there's a single blocked visit, this ensures the site is removed 165 | // upon the next PAC update. 166 | vc.visit(&vc.Blocked) 167 | vc.Direct = 0 168 | } 169 | 170 | type SiteStat struct { 171 | Update Date `json:"update"` 172 | Vcnt map[string]*VisitCnt `json:"site_info"` // Vcnt uses host as key 173 | vcLock sync.RWMutex 174 | 175 | // Whether a domain has blocked host. Used to avoid considering a domain as 176 | // direct though it has blocked hosts. 177 | hasBlockedHost map[string]bool 178 | hbhLock sync.RWMutex 179 | } 180 | 181 | func newSiteStat() *SiteStat { 182 | return &SiteStat{ 183 | Vcnt: map[string]*VisitCnt{}, 184 | hasBlockedHost: map[string]bool{}, 185 | } 186 | } 187 | 188 | func (ss *SiteStat) get(s string) *VisitCnt { 189 | ss.vcLock.RLock() 190 | Vcnt, ok := ss.Vcnt[s] 191 | ss.vcLock.RUnlock() 192 | if ok { 193 | return Vcnt 194 | } 195 | return nil 196 | } 197 | 198 | func (ss *SiteStat) create(s string) (vcnt *VisitCnt) { 199 | vcnt = newVisitCnt(0, 0) 200 | ss.vcLock.Lock() 201 | ss.Vcnt[s] = vcnt 202 | ss.vcLock.Unlock() 203 | return 204 | } 205 | 206 | // Caller should guarantee that always direct url does not attempt 207 | // blocked visit. 208 | func (ss *SiteStat) TempBlocked(url *URL) { 209 | debug.Printf("%s temp blocked\n", url.Host) 210 | 211 | vcnt := ss.get(url.Host) 212 | if vcnt == nil { 213 | panic("TempBlocked should always get existing visitCnt") 214 | } 215 | vcnt.tempBlocked() 216 | 217 | // Mistakenly consider a partial blocked domain as direct will make that 218 | // domain into PAC and never have a chance to correct the error. 219 | // Once using blocked visit, a host is considered to maybe blocked even if 220 | // it's block visit count decrease to 0. As hasBlockedHost is not saved, 221 | // upon next start up of COW, the information will reflect the current 222 | // status of that host. 223 | ss.hbhLock.RLock() 224 | t := ss.hasBlockedHost[url.Domain] 225 | ss.hbhLock.RUnlock() 226 | if !t { 227 | ss.hbhLock.Lock() 228 | ss.hasBlockedHost[url.Domain] = true 229 | ss.hbhLock.Unlock() 230 | } 231 | } 232 | 233 | var alwaysDirectVisitCnt = newVisitCnt(userCnt, 0) 234 | 235 | func (ss *SiteStat) GetVisitCnt(url *URL) (vcnt *VisitCnt) { 236 | if parentProxy.empty() { // no way to retry, so always visit directly 237 | return alwaysDirectVisitCnt 238 | } 239 | if url.Domain == "" { // simple host or private ip 240 | return alwaysDirectVisitCnt 241 | } 242 | if vcnt = ss.get(url.Host); vcnt != nil { 243 | return 244 | } 245 | if len(url.Domain) != len(url.Host) { 246 | if dmcnt := ss.get(url.Domain); dmcnt != nil && dmcnt.userSpecified() { 247 | // if the domain is not specified by user, should create a new host 248 | // visitCnt 249 | return dmcnt 250 | } 251 | } 252 | return ss.create(url.Host) 253 | } 254 | 255 | func (ss *SiteStat) store(statPath string) (err error) { 256 | now := time.Now() 257 | var savedSS *SiteStat 258 | if ss.Update == Date(zeroTime) { 259 | ss.Update = Date(time.Now()) 260 | } 261 | if now.Sub(time.Time(ss.Update)) > siteStaleThreshold { 262 | // Not updated for a long time, don't drop any record 263 | savedSS = ss 264 | // Changing update time too fast will also drop useful record 265 | savedSS.Update = Date(time.Time(ss.Update).Add(siteStaleThreshold / 2)) 266 | if time.Time(savedSS.Update).After(now) { 267 | savedSS.Update = Date(now) 268 | } 269 | } else { 270 | savedSS = newSiteStat() 271 | savedSS.Update = Date(now) 272 | ss.vcLock.RLock() 273 | for site, vcnt := range ss.Vcnt { 274 | if vcnt.shouldNotSave() { 275 | continue 276 | } 277 | savedSS.Vcnt[site] = vcnt 278 | } 279 | ss.vcLock.RUnlock() 280 | } 281 | 282 | b, err := json.MarshalIndent(savedSS, "", "\t") 283 | if err != nil { 284 | errl.Println("Error marshalling site stat:", err) 285 | panic("internal error: error marshalling site") 286 | } 287 | 288 | // Store stat into temp file first and then rename. 289 | // Ensures atomic update to stat file to avoid file damage. 290 | 291 | // Create tmp file inside config firectory to avoid cross FS rename. 292 | f, err := ioutil.TempFile(config.dir, "stat") 293 | if err != nil { 294 | errl.Println("create tmp file to store stat", err) 295 | return 296 | } 297 | if _, err = f.Write(b); err != nil { 298 | errl.Println("Error writing stat file:", err) 299 | f.Close() 300 | return 301 | } 302 | f.Close() 303 | 304 | // Windows don't allow rename to existing file. 305 | os.Remove(statPath + ".bak") 306 | os.Rename(statPath, statPath+".bak") 307 | if err = os.Rename(f.Name(), statPath); err != nil { 308 | errl.Println("rename new stat file", err) 309 | return 310 | } 311 | return 312 | } 313 | 314 | func (ss *SiteStat) loadList(lst []string, direct, blocked vcntint) { 315 | for _, d := range lst { 316 | ss.Vcnt[d] = newVisitCntWithTime(direct, blocked, zeroTime) 317 | } 318 | } 319 | 320 | func (ss *SiteStat) loadBuiltinList() { 321 | ss.loadList(blockedDomainList, 0, userCnt) 322 | ss.loadList(directDomainList, userCnt, 0) 323 | } 324 | 325 | func (ss *SiteStat) loadUserList() { 326 | if directList, err := loadSiteList(config.DirectFile); err == nil { 327 | ss.loadList(directList, userCnt, 0) 328 | } 329 | if blockedList, err := loadSiteList(config.BlockedFile); err == nil { 330 | ss.loadList(blockedList, 0, userCnt) 331 | } 332 | } 333 | 334 | // Filter sites covered by user specified domains, also filter out stale 335 | // sites. 336 | func (ss *SiteStat) filterSites() { 337 | // It's not safe to remove element while iterating over a map. 338 | var removeSites []string 339 | 340 | // find what to remove first 341 | ss.vcLock.RLock() 342 | for site, vcnt := range ss.Vcnt { 343 | if vcnt.userSpecified() { 344 | continue 345 | } 346 | if vcnt.isStale() { 347 | removeSites = append(removeSites, site) 348 | continue 349 | } 350 | var dmcnt *VisitCnt 351 | domain := host2Domain(site) 352 | if domain != site { 353 | dmcnt = ss.get(domain) 354 | } 355 | if dmcnt != nil && dmcnt.userSpecified() { 356 | removeSites = append(removeSites, site) 357 | } 358 | } 359 | ss.vcLock.RUnlock() 360 | 361 | // do remove 362 | ss.vcLock.Lock() 363 | for _, site := range removeSites { 364 | delete(ss.Vcnt, site) 365 | } 366 | ss.vcLock.Unlock() 367 | } 368 | 369 | func (ss *SiteStat) load(file string) (err error) { 370 | defer func() { 371 | // load builtin list first, so user list can override builtin 372 | ss.loadBuiltinList() 373 | ss.loadUserList() 374 | ss.filterSites() 375 | for host, vcnt := range ss.Vcnt { 376 | if vcnt.OnceBlocked() { 377 | ss.hasBlockedHost[host2Domain(host)] = true 378 | } 379 | } 380 | }() 381 | if file == "" { 382 | return 383 | } 384 | if err = isFileExists(file); err != nil { 385 | if !os.IsNotExist(err) { 386 | errl.Println("Error loading stat:", err) 387 | } 388 | return 389 | } 390 | var f *os.File 391 | if f, err = os.Open(file); err != nil { 392 | errl.Printf("Error opening site stat %s: %v\n", file, err) 393 | return 394 | } 395 | defer f.Close() 396 | b, err := ioutil.ReadAll(f) 397 | if err != nil { 398 | errl.Println("Error reading site stat:", err) 399 | return 400 | } 401 | if err = json.Unmarshal(b, ss); err != nil { 402 | errl.Println("Error decoding site stat:", err) 403 | return 404 | } 405 | return 406 | } 407 | 408 | func (ss *SiteStat) GetDirectList() []string { 409 | lst := make([]string, 0) 410 | // anyway to do more fine grained locking? 411 | ss.vcLock.RLock() 412 | for site, vc := range ss.Vcnt { 413 | if ss.hasBlockedHost[host2Domain(site)] { 414 | continue 415 | } 416 | if vc.AsDirect() { 417 | lst = append(lst, site) 418 | } 419 | } 420 | ss.vcLock.RUnlock() 421 | return lst 422 | } 423 | 424 | var siteStat = newSiteStat() 425 | 426 | func initSiteStat() { 427 | err := siteStat.load(config.StatFile) 428 | if err != nil { 429 | // Simply try to load the stat.back, create a new object to avoid error 430 | // in default site list. 431 | siteStat = newSiteStat() 432 | err = siteStat.load(config.StatFile + ".bak") 433 | // After all its not critical , simply re-create a stat object if anything is not ok 434 | if err != nil { 435 | siteStat = newSiteStat() 436 | siteStat.load("") // load default site list 437 | } 438 | } 439 | 440 | // Dump site stat while running, so we don't always need to close cow to 441 | // get updated stat. 442 | go func() { 443 | for { 444 | time.Sleep(5 * time.Minute) 445 | storeSiteStat(siteStatCont) 446 | } 447 | }() 448 | } 449 | 450 | const ( 451 | siteStatExit = iota 452 | siteStatCont 453 | ) 454 | 455 | // Lock ensures only one goroutine calling store. 456 | // siteStatFini ensures no more calls after going to exit. 457 | var storeLock sync.Mutex 458 | var siteStatFini bool 459 | 460 | func storeSiteStat(cont byte) { 461 | storeLock.Lock() 462 | defer storeLock.Unlock() 463 | 464 | if siteStatFini { 465 | return 466 | } 467 | siteStat.store(config.StatFile) 468 | if cont == siteStatExit { 469 | siteStatFini = true 470 | } 471 | } 472 | 473 | func loadSiteList(fpath string) (lst []string, err error) { 474 | if fpath == "" { 475 | return 476 | } 477 | if err = isFileExists(fpath); err != nil { 478 | if !os.IsNotExist(err) { 479 | info.Printf("Error loading domaint list: %v\n", err) 480 | } 481 | return 482 | } 483 | f, err := os.Open(fpath) 484 | if err != nil { 485 | errl.Println("Error opening domain list:", err) 486 | return 487 | } 488 | defer f.Close() 489 | 490 | scanner := bufio.NewScanner(f) 491 | lst = make([]string, 0) 492 | for scanner.Scan() { 493 | site := strings.TrimSpace(scanner.Text()) 494 | if site == "" { 495 | continue 496 | } 497 | lst = append(lst, site) 498 | } 499 | if scanner.Err() != nil { 500 | errl.Printf("Error reading domain list %s: %v\n", fpath, scanner.Err()) 501 | } 502 | return lst, scanner.Err() 503 | } 504 | -------------------------------------------------------------------------------- /sitestat_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var _ = os.Remove 10 | 11 | func TestNetworkBad(t *testing.T) { 12 | if networkBad() { 13 | t.Error("Network by default should be good") 14 | } 15 | } 16 | 17 | func TestDateMarshal(t *testing.T) { 18 | d := Date(time.Date(2013, 2, 4, 0, 0, 0, 0, time.UTC)) 19 | j, err := d.MarshalJSON() 20 | if err != nil { 21 | t.Error("Error marshalling json:", err) 22 | } 23 | if string(j) != "\"2013-02-04\"" { 24 | t.Error("Date marshal result wrong, got:", string(j)) 25 | } 26 | 27 | err = d.UnmarshalJSON([]byte("\"2013-01-01\"")) 28 | if err != nil { 29 | t.Error("Error unmarshaling Date:", err) 30 | } 31 | tm := time.Time(d) 32 | if tm.Year() != 2013 || tm.Month() != 1 || tm.Day() != 1 { 33 | t.Error("Unmarshaled date wrong, got:", tm) 34 | } 35 | } 36 | 37 | func TestSiteStatLoadStore(t *testing.T) { 38 | ss := newSiteStat() 39 | ss.load("testdata/nosuchfile") // load buildin and user specified list 40 | if len(ss.GetDirectList()) == 0 { 41 | t.Error("builtin site should appear in direct site list even with no stat file") 42 | } 43 | 44 | url1, _ := ParseRequestURI("www.foobar.com") 45 | url2, _ := ParseRequestURI("img.foobar.com") 46 | vcnt1 := ss.GetVisitCnt(url1) 47 | vcnt1.DirectVisit() 48 | vcnt1.DirectVisit() 49 | vcnt1.DirectVisit() 50 | vcnt2 := ss.GetVisitCnt(url2) 51 | vcnt2.DirectVisit() 52 | 53 | blockurl1, _ := ParseRequestURI("blocked.com") 54 | blockurl2, _ := ParseRequestURI("blockeurl2.com") 55 | si1 := ss.GetVisitCnt(blockurl1) 56 | si1.BlockedVisit() 57 | si2 := ss.GetVisitCnt(blockurl2) 58 | si2.BlockedVisit() 59 | 60 | // make google.com with a large direct count, but plus.google.com is in blocked list 61 | // so it shouldn't be considered as direct site 62 | gurl, _ := ParseRequestURI("google.com") 63 | gvcnt := ss.GetVisitCnt(gurl) 64 | gvcnt.Direct = 100 65 | 66 | const stfile = "testdata/stat" 67 | if err := ss.store(stfile); err != nil { 68 | t.Fatal("store error:", err) 69 | } 70 | 71 | ld := newSiteStat() 72 | if err := ld.load(stfile); err != nil { 73 | t.Fatal("load stat error:", err) 74 | } 75 | vc := ld.get(url1.Host) 76 | if vc == nil { 77 | t.Fatalf("load error, %s not loaded\n", url1.Host) 78 | } 79 | if vc.Direct != 3 { 80 | t.Errorf("load error, %s should have visit cnt 3, got: %d\n", url1.Host, vc.Direct) 81 | } 82 | 83 | vc = ld.get(blockurl1.Host) 84 | if vc == nil { 85 | t.Errorf("load error, %s not loaded\n", blockurl1.Host) 86 | } 87 | 88 | // test bulitin site 89 | ap, _ := ParseRequestURI("apple.com") 90 | si := ld.GetVisitCnt(ap) 91 | if !si.AlwaysDirect() { 92 | t.Error("builtin site apple.com should always use direct access") 93 | } 94 | tw, _ := ParseRequestURI("twitter.com") 95 | si = ld.GetVisitCnt(tw) 96 | if !si.AsBlocked() || !si.AlwaysBlocked() { 97 | t.Error("builtin site twitter.com should use blocked access") 98 | } 99 | plus, _ := ParseRequestURI("plus.google.com") 100 | si = ld.GetVisitCnt(plus) 101 | if !si.AsBlocked() || !si.AlwaysBlocked() { 102 | t.Error("builtin site plus.google.com should use blocked access") 103 | } 104 | 105 | directList := ld.GetDirectList() 106 | if len(directList) == 0 { 107 | t.Error("builtin site should appear in direct site list") 108 | } 109 | if !ld.hasBlockedHost["google.com"] { 110 | t.Error("google.com should have blocked host") 111 | } 112 | for _, d := range directList { 113 | if d == "google.com" { 114 | t.Errorf("direct list contains 2nd level domain which has sub host that's blocked") 115 | } 116 | } 117 | os.Remove(stfile) 118 | } 119 | 120 | func TestSiteStatVisitCnt(t *testing.T) { 121 | ss := newSiteStat() 122 | 123 | g1, _ := ParseRequestURI("www.gtemp.com") 124 | g2, _ := ParseRequestURI("calendar.gtemp.com") 125 | g3, _ := ParseRequestURI("docs.gtemp.com") 126 | 127 | sg1 := ss.GetVisitCnt(g1) 128 | for i := 0; i < 30; i++ { 129 | sg1.DirectVisit() 130 | } 131 | sg2 := ss.GetVisitCnt(g2) 132 | sg2.DirectVisit() 133 | sg3 := ss.GetVisitCnt(g3) 134 | sg3.DirectVisit() 135 | 136 | if ss.hasBlockedHost[g1.Domain] { 137 | t.Errorf("direct domain %s should not have host at first\n", g1.Domain) 138 | } 139 | 140 | vc := ss.get(g1.Host) 141 | if vc == nil { 142 | t.Fatalf("no VisitCnt for %s\n", g1.Host) 143 | } 144 | if vc.Direct != 30 { 145 | t.Errorf("direct cnt for %s not correct, should be 30, got: %d\n", g1.Host, vc.Direct) 146 | } 147 | if vc.Blocked != 0 { 148 | t.Errorf("block cnt for %s not correct, should be 0 before blocked visit, got: %d\n", g1.Host, vc.Blocked) 149 | } 150 | if vc.rUpdated != true { 151 | t.Errorf("VisitCnt lvUpdated should be true after visit") 152 | } 153 | 154 | vc.BlockedVisit() 155 | if vc.Blocked != 1 { 156 | t.Errorf("blocked cnt for %s after 1 blocked visit should be 1, got: %d\n", g1.Host, vc.Blocked) 157 | } 158 | if vc.Direct != 0 { 159 | t.Errorf("direct cnt for %s after 1 blocked visit should be 0, got: %d\n", g1.Host, vc.Direct) 160 | } 161 | if vc.AsDirect() { 162 | t.Errorf("after blocked visit, a site should not be considered as direct\n") 163 | } 164 | 165 | // test blocked visit 166 | g4, _ := ParseRequestURI("plus.gtemp.com") 167 | si := ss.GetVisitCnt(g4) 168 | ss.TempBlocked(g4) 169 | // should be blocked for 2 minutes 170 | if !si.AsTempBlocked() { 171 | t.Error("should be blocked for 2 minutes after blocked visit") 172 | } 173 | si.BlockedVisit() // After temp blocked, update blocked visit count 174 | if si.Blocked != 1 { 175 | t.Errorf("blocked cnt for %s not correct, should be 1, got: %d\n", g4.Host, vc.Blocked) 176 | } 177 | vc = ss.get(g4.Host) 178 | if vc == nil { 179 | t.Fatal("no VisitCnt for ", g4.Host) 180 | } 181 | if vc.Direct != 0 { 182 | t.Errorf("direct cnt for %s not correct, should be 0, got: %d\n", g4.Host, vc.Direct) 183 | } 184 | if !ss.hasBlockedHost[g4.Domain] { 185 | t.Errorf("direct domain %s should have blocked host after blocked visit\n", g4.Domain) 186 | } 187 | } 188 | 189 | func TestSiteStatGetVisitCnt(t *testing.T) { 190 | ss := newSiteStat() 191 | 192 | g, _ := ParseRequestURI("gtemp.com") 193 | si := ss.GetVisitCnt(g) 194 | if !si.AsDirect() { 195 | t.Error("never visited site should be considered as direct") 196 | } 197 | if si.AsBlocked() || si.AsTempBlocked() { 198 | t.Error("never visited site should not be considered as blocked/temp blocked") 199 | } 200 | si.DirectVisit() 201 | gw, _ := ParseRequestURI("www.gtemp.com") 202 | sig := ss.GetVisitCnt(gw) 203 | // gtemp.com is not user specified, www.gtemp.com should get separate visitCnt 204 | if sig == si { 205 | t.Error("host should get separate visitCnt for not user specified domain") 206 | } 207 | 208 | b, _ := ParseRequestURI("www.btemp.com") 209 | ss.Vcnt[b.Host] = newVisitCnt(userCnt, 0) 210 | vc := ss.get(b.Host) 211 | if !vc.userSpecified() { 212 | t.Error("should be user specified") 213 | } 214 | if !vc.shouldNotSave() { 215 | t.Error("user specified should be dropped") 216 | } 217 | si = ss.GetVisitCnt(b) 218 | if !si.AlwaysDirect() { 219 | t.Errorf("%s should alwaysDirect\n", b.Host) 220 | } 221 | if si.AlwaysBlocked() { 222 | t.Errorf("%s should not alwaysBlocked\n", b.Host) 223 | } 224 | if si.OnceBlocked() { 225 | t.Errorf("%s should not onceBlocked\n", b.Host) 226 | } 227 | if !si.AsDirect() { 228 | t.Errorf("%s should use direct visit\n", b.Host) 229 | } 230 | 231 | tw, _ := ParseRequestURI("www.tblocked.com") 232 | ss.Vcnt[tw.Domain] = newVisitCnt(0, userCnt) 233 | si = ss.GetVisitCnt(tw) 234 | if !si.AsBlocked() { 235 | t.Errorf("%s should use blocked visit\n", tw.Host) 236 | } 237 | if si.AlwaysDirect() { 238 | t.Errorf("%s should not alwaysDirect\n", tw.Host) 239 | } 240 | if !si.AlwaysBlocked() { 241 | t.Errorf("%s should not alwaysBlocked\n", tw.Host) 242 | } 243 | if !si.OnceBlocked() { 244 | t.Errorf("%s should onceBlocked\n", tw.Host) 245 | } 246 | 247 | g1, _ := ParseRequestURI("www.shoulddirect.com") 248 | for i := 0; i < directDelta; i++ { 249 | si.DirectVisit() 250 | } 251 | si = ss.GetVisitCnt(g1) 252 | if !si.AsDirect() { 253 | t.Errorf("%s direct %d times, should use direct visit\n", g1.Host, directDelta+1) 254 | } 255 | if si.OnceBlocked() { 256 | t.Errorf("%s has not blocked visit, should not has once blocked\n", g1.Host) 257 | } 258 | si = ss.GetVisitCnt(g1) 259 | si.BlockedVisit() 260 | if !si.OnceBlocked() { 261 | t.Errorf("%s has one blocked visit, should has once blocked\n", g1.Host) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /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/cyfdecyf/cow/41c0fb157c8b939b724ae0d58dad3a1b7cd2e811/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 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "os" 11 | "path" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/cyfdecyf/bufio" 17 | ) 18 | 19 | const isWindows = runtime.GOOS == "windows" 20 | 21 | type notification chan byte 22 | 23 | func newNotification() notification { 24 | // Notification channle has size 1, so sending a single one will not block 25 | return make(chan byte, 1) 26 | } 27 | 28 | func (n notification) notify() { 29 | n <- 1 30 | } 31 | 32 | func (n notification) hasNotified() bool { 33 | select { 34 | case <-n: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | func ASCIIToUpperInplace(b []byte) { 42 | for i := 0; i < len(b); i++ { 43 | if 97 <= b[i] && b[i] <= 122 { 44 | b[i] -= 32 45 | } 46 | } 47 | } 48 | 49 | func ASCIIToUpper(b []byte) []byte { 50 | buf := make([]byte, len(b)) 51 | for i := 0; i < len(b); i++ { 52 | if 97 <= b[i] && b[i] <= 122 { 53 | buf[i] = b[i] - 32 54 | } else { 55 | buf[i] = b[i] 56 | } 57 | } 58 | return buf 59 | } 60 | 61 | func ASCIIToLowerInplace(b []byte) { 62 | for i := 0; i < len(b); i++ { 63 | if 65 <= b[i] && b[i] <= 90 { 64 | b[i] += 32 65 | } 66 | } 67 | } 68 | 69 | func ASCIIToLower(b []byte) []byte { 70 | buf := make([]byte, len(b)) 71 | for i := 0; i < len(b); i++ { 72 | if 65 <= b[i] && b[i] <= 90 { 73 | buf[i] = b[i] + 32 74 | } else { 75 | buf[i] = b[i] 76 | } 77 | } 78 | return buf 79 | } 80 | 81 | func IsDigit(b byte) bool { 82 | return '0' <= b && b <= '9' 83 | } 84 | 85 | var spaceTbl = [256]bool{ 86 | '\t': true, // ht 87 | '\n': true, // lf 88 | '\r': true, // cr 89 | ' ': true, // sp 90 | } 91 | 92 | func IsSpace(b byte) bool { 93 | return spaceTbl[b] 94 | } 95 | 96 | func TrimSpace(s []byte) []byte { 97 | st := 0 98 | end := len(s) - 1 99 | for ; st < len(s) && IsSpace(s[st]); st++ { 100 | } 101 | if st == len(s) { 102 | return s[:0] 103 | } 104 | for ; end >= 0 && IsSpace(s[end]); end-- { 105 | } 106 | return s[st : end+1] 107 | } 108 | 109 | func TrimTrailingSpace(s []byte) []byte { 110 | end := len(s) - 1 111 | for ; end >= 0 && IsSpace(s[end]); end-- { 112 | } 113 | return s[:end+1] 114 | } 115 | 116 | // FieldsN is simliar with bytes.Fields, but only consider space and '\t' as 117 | // space, and will include all content in the final slice with ending white 118 | // space characters trimmed. bytes.Split can't split on both space and '\t', 119 | // and considers two separator as an empty item. bytes.FieldsFunc can't 120 | // specify how much fields we need, which is required for parsing response 121 | // status line. Returns nil if n < 0. 122 | func FieldsN(s []byte, n int) [][]byte { 123 | if n <= 0 { 124 | return nil 125 | } 126 | res := make([][]byte, n) 127 | na := 0 128 | fieldStart := -1 129 | var i int 130 | for ; i < len(s); i++ { 131 | issep := s[i] == ' ' || s[i] == '\t' 132 | if fieldStart < 0 && !issep { 133 | fieldStart = i 134 | } 135 | if fieldStart >= 0 && issep { 136 | if na == n-1 { 137 | break 138 | } 139 | res[na] = s[fieldStart:i] 140 | na++ 141 | fieldStart = -1 142 | } 143 | } 144 | if fieldStart >= 0 { // must have na <= n-1 here 145 | res[na] = TrimSpace(s[fieldStart:]) 146 | if len(res[na]) != 0 { // do not consider ending space as a field 147 | na++ 148 | } 149 | } 150 | return res[:na] 151 | } 152 | 153 | var digitTbl = [256]int8{ 154 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 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 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, 158 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, 159 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 160 | -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, 161 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 162 | } 163 | 164 | // ParseIntFromBytes parse hexidecimal number from given bytes. 165 | // No prefix (e.g. 0xdeadbeef) should given. 166 | // base can only be 10 or 16. 167 | func ParseIntFromBytes(b []byte, base int) (n int64, err error) { 168 | // Currently, we have to convert []byte to string to use strconv 169 | // Refer to: http://code.google.com/p/go/issues/detail?id=2632 170 | // That's why I created this function. 171 | if base != 10 && base != 16 { 172 | err = errors.New(fmt.Sprintf("invalid base: %d", base)) 173 | return 174 | } 175 | if len(b) == 0 { 176 | err = errors.New("parse int from empty bytes") 177 | return 178 | } 179 | 180 | neg := false 181 | if b[0] == '+' { 182 | b = b[1:] 183 | } else if b[0] == '-' { 184 | b = b[1:] 185 | neg = true 186 | } 187 | 188 | for _, d := range b { 189 | v := digitTbl[d] 190 | if v == -1 { 191 | n = 0 192 | err = errors.New(fmt.Sprintf("invalid number: %s", b)) 193 | return 194 | } 195 | if int(v) >= base { 196 | n = 0 197 | err = errors.New(fmt.Sprintf("invalid base %d number: %s", base, b)) 198 | return 199 | } 200 | n *= int64(base) 201 | n += int64(v) 202 | } 203 | if neg { 204 | n = -n 205 | } 206 | return 207 | } 208 | 209 | func isFileExists(path string) error { 210 | stat, err := os.Stat(path) 211 | if err != nil { 212 | return err 213 | } 214 | if !stat.Mode().IsRegular() { 215 | return fmt.Errorf("%s is not regular file", path) 216 | } 217 | return nil 218 | } 219 | 220 | func isDirExists(path string) error { 221 | stat, err := os.Stat(path) 222 | if err != nil { 223 | return err 224 | } 225 | if !stat.IsDir() { 226 | return fmt.Errorf("%s is not directory", path) 227 | } 228 | return nil 229 | } 230 | 231 | func getUserHomeDir() string { 232 | home := os.Getenv("HOME") 233 | if home == "" { 234 | fmt.Println("HOME environment variable is empty") 235 | } 236 | return home 237 | } 238 | 239 | func expandTilde(pth string) string { 240 | if len(pth) > 0 && pth[0] == '~' { 241 | home := getUserHomeDir() 242 | return path.Join(home, pth[1:]) 243 | } 244 | return pth 245 | } 246 | 247 | // copyN copys N bytes from src to dst, reading at most rdSize for each read. 248 | // rdSize should <= buffer size of the buffered reader. 249 | // Returns any encountered error. 250 | func copyN(dst io.Writer, src *bufio.Reader, n, rdSize int) (err error) { 251 | // Most of the copy is copied from io.Copy 252 | for n > 0 { 253 | var b []byte 254 | var er error 255 | if n > rdSize { 256 | b, er = src.ReadN(rdSize) 257 | } else { 258 | b, er = src.ReadN(n) 259 | } 260 | nr := len(b) 261 | n -= nr 262 | if nr > 0 { 263 | nw, ew := dst.Write(b) 264 | if ew != nil { 265 | err = ew 266 | break 267 | } 268 | if nr != nw { 269 | err = io.ErrShortWrite 270 | break 271 | } 272 | } 273 | if er == io.EOF { 274 | break 275 | } 276 | if er != nil { 277 | err = er 278 | break 279 | } 280 | } 281 | return err 282 | } 283 | 284 | func md5sum(ss ...string) string { 285 | h := md5.New() 286 | for _, s := range ss { 287 | io.WriteString(h, s) 288 | } 289 | return fmt.Sprintf("%x", h.Sum(nil)) 290 | } 291 | 292 | // hostIsIP determines whether a host address is an IP address and whether 293 | // it is private. Currenly only handles IPv4 addresses. 294 | func hostIsIP(host string) (isIP, isPrivate bool) { 295 | part := strings.Split(host, ".") 296 | if len(part) != 4 { 297 | return false, false 298 | } 299 | for _, i := range part { 300 | if len(i) == 0 || len(i) > 3 { 301 | return false, false 302 | } 303 | n, err := strconv.Atoi(i) 304 | if err != nil || n < 0 || n > 255 { 305 | return false, false 306 | } 307 | } 308 | if part[0] == "127" || part[0] == "10" || (part[0] == "192" && part[1] == "168") { 309 | return true, true 310 | } 311 | if part[0] == "172" { 312 | n, _ := strconv.Atoi(part[1]) 313 | if 16 <= n && n <= 31 { 314 | return true, true 315 | } 316 | } 317 | return true, false 318 | } 319 | 320 | // NetNbitIPv4Mask returns a IPMask with highest n bit set. 321 | func NewNbitIPv4Mask(n int) net.IPMask { 322 | if n > 32 { 323 | panic("NewNbitIPv4Mask: bit number > 32") 324 | } 325 | mask := []byte{0, 0, 0, 0} 326 | for id := 0; id < 4; id++ { 327 | if n >= 8 { 328 | mask[id] = 0xff 329 | } else { 330 | mask[id] = ^byte(1<<(uint8(8-n)) - 1) 331 | break 332 | } 333 | n -= 8 334 | } 335 | return net.IPMask(mask) 336 | } 337 | 338 | var topLevelDomain = map[string]bool{ 339 | "ac": true, 340 | "co": true, 341 | "com": true, 342 | "edu": true, 343 | "gov": true, 344 | "net": true, 345 | "org": true, 346 | } 347 | 348 | func trimLastDot(s string) string { 349 | if len(s) > 0 && s[len(s)-1] == '.' { 350 | return s[:len(s)-1] 351 | } 352 | return s 353 | } 354 | 355 | // host2Domain returns the domain of a host. It will recognize domains like 356 | // google.com.hk. Returns empty string for simple host and internal IP. 357 | func host2Domain(host string) (domain string) { 358 | isIP, isPrivate := hostIsIP(host) 359 | if isPrivate { 360 | return "" 361 | } 362 | if isIP { 363 | return host 364 | } 365 | host = trimLastDot(host) 366 | lastDot := strings.LastIndex(host, ".") 367 | if lastDot == -1 { 368 | return "" 369 | } 370 | // Find the 2nd last dot 371 | dot2ndLast := strings.LastIndex(host[:lastDot], ".") 372 | if dot2ndLast == -1 { 373 | return host 374 | } 375 | 376 | part := host[dot2ndLast+1 : lastDot] 377 | // If the 2nd last part of a domain name equals to a top level 378 | // domain, search for the 3rd part in the host name. 379 | // So domains like bbc.co.uk will not be recorded as co.uk 380 | if topLevelDomain[part] { 381 | dot3rdLast := strings.LastIndex(host[:dot2ndLast], ".") 382 | if dot3rdLast == -1 { 383 | return host 384 | } 385 | return host[dot3rdLast+1:] 386 | } 387 | return host[dot2ndLast+1:] 388 | } 389 | 390 | // IgnoreUTF8BOM consumes UTF-8 encoded BOM character if present in the file. 391 | func IgnoreUTF8BOM(f *os.File) error { 392 | bom := make([]byte, 3) 393 | n, err := f.Read(bom) 394 | if err != nil { 395 | return err 396 | } 397 | if n != 3 { 398 | return nil 399 | } 400 | if bytes.Equal(bom, []byte{0xEF, 0xBB, 0xBF}) { 401 | debug.Println("UTF-8 BOM found") 402 | return nil 403 | } 404 | // No BOM found, seek back 405 | _, err = f.Seek(-3, 1) 406 | return err 407 | } 408 | 409 | // Return all host IP addresses. 410 | func hostAddr() (addr []string) { 411 | allAddr, err := net.InterfaceAddrs() 412 | if err != nil { 413 | Fatal("error getting host address", err) 414 | } 415 | for _, ad := range allAddr { 416 | ads := ad.String() 417 | id := strings.Index(ads, "/") 418 | if id == -1 { 419 | // On windows, no network mask. 420 | id = len(ads) 421 | } 422 | addr = append(addr, ads[:id]) 423 | } 424 | return addr 425 | } 426 | -------------------------------------------------------------------------------- /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", "sina.com.cn"}, 256 | {"www.bbc.co.uk", "bbc.co.uk"}, 257 | {"apple.com.cn", "apple.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 | --------------------------------------------------------------------------------