├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── conf.yml ├── filter ├── cookie.go ├── header.go ├── header_test.go ├── request.go └── response.go ├── grp.sh ├── log4go.xml ├── main.go ├── midware └── router.go ├── model └── model.go └── util └── io ├── ioutil.go └── ioutil_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.iml 27 | bin/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 ZHU HAIHUA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GRP (Go Reverse Proxy) 是一个用Golang实现的反向代理服务器。 2 | 3 | ### 现有方案 4 | 5 | 目前已有的相对简单的反向代理网站的解决方案包括使用nginx的方案,还有一个基于PHP的g7host。 6 | 7 | 前者非常灵活,但是需要root权限,网上已有专门针对google的Nginx模块,但需要自行编译, 如果要自己配置Nginx其实也比较复杂。 8 | 9 | 后者简单,并且有简易的配置界面,基于PHP的方案也可以找到一些免费主机来支持, 但是我尝试了一下, 发现这个方案至少访问Google是有问题的, 并没有办法正常使用。 10 | 11 | ### GRP简介 12 | 13 | GRP 希望让部署和配置简单化, 启动可以无需root权限, 并且能够在一台服务器上代理多个网站。 14 | 15 | 需要同时代理多个网站的时候, 提供多个不同的子域名, 可以在DNS服务器中指定这些子域名执行您部署GRP的服务器。 16 | 17 | 如果你没有自己的DNS服务器, 或者无法指定多个子域名,也可以通过顶级域名或者IP地址访问,但由于顶级域名和IP地址只有一个,所以这时候只能支持代理单个网站。 18 | 19 | > 现在网上其实可以申请到免费的空间以及免费的二级域名解析服务。大家可以自行搜索。所以这其实都不是问题。;) 20 | > 当然有条件的, 使用自己购买的主机和域名, 质量还是会更有保证。 21 | 22 | 配置示例文件在[conf.yml](https://github.com/kimiazhu/grp/blob/master/conf.yml)中。 23 | 24 | ### 使用 25 | 26 | 分发版本包括三个文件: 27 | 28 | * grp -- 可执行文件, 包括Linux版, Mac版和Windows版; 29 | * conf.yml -- 配置文件, 本地要使用哪个端口, 你通过哪个子域名代理哪个站点, 都在这里进行配置; 30 | * log4go.xml -- 日志配置文件, 控制日志如何输出, 这个你可以选择忽略。 31 | 32 | 将以上三个文件上传到你的服务器中, 直接执行即可, 例如: 33 | 34 | ```bash 35 | $ nohup ./grp > /dev/null 2>stderr.log & 36 | ``` 37 | 38 | ### 支持网站列表 39 | 40 | 已测试网站列表: 41 | 42 | - Google搜索 43 | - 百度搜索 44 | - Wikipedia(中文页面) 45 | 46 | ### TODO 47 | 48 | - 支持一个本地子域名对应多个远程站点,无需DNS配置多个子域名的情况代理多个远程地址。(P0) 49 | - SSL支持,当本地服务不是起在80和443端口时,在做域名替换的时候要根据远程服务器的Schema或者端口号智能切换本地端口号。这个问题目前在登录accounts.google.com时会出现。(P1) 50 | - 登录和会话保持。(P1) 51 | - on-file-change auto reload 52 | - multi backend services and load balance support 53 | - 自动感知网站是否使用https? 避免手工在配置文件中进行配置。 54 | - 支持通过顶级域名代理网站同一顶级域名下对应的所有子域名。 55 | 56 | ### Change Log 57 | 58 | ##### 2016/08/10 59 | 60 | - Initial Commit 61 | - 支持Google和百度搜索, zh.wikipedia.org 62 | 63 | ##### 2016/08/14 64 | 65 | - grp.sh script for Linux 66 | 67 | ##### 2016/08/18 68 | 69 | - refactor && bug fixes 70 | - handle compressed response, replaced all target host name. -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf "bin/grp_linux" 4 | export set GOOS=linux 5 | export set GOARCH=amd64 6 | go build -o bin/grp_linux 7 | 8 | rm -rf "bin/grp_darwin" 9 | export set GOOS=darwin 10 | export set GOARCH=amd64 11 | go build -o bin/grp_darwin 12 | 13 | rm -rf "bin/grp.exe" 14 | export set GOOS=windows 15 | export set GOARCH=amd64 16 | go build -o bin/grp.exe 17 | -------------------------------------------------------------------------------- /conf.yml: -------------------------------------------------------------------------------- 1 | server: 2 | runmode: debug #or debug/test/release 3 | port: 8888 4 | 5 | ext: 6 | versionName: "0.0.1" 7 | versionCode: 1 8 | 9 | proxy: 10 | google: 11 | remote: "www.google.com" 12 | remoteSchema: "https" 13 | local: "google.localhost.com" 14 | googleapis: 15 | remote: "apis.google.com" 16 | remoteSchema: "https" 17 | local: "googleapis.localhost.com" 18 | gmail: 19 | remote: "mail.google.com" 20 | remoteSchema: "https" 21 | local: "gmail.localhost.com" 22 | googleAccount: 23 | remote: "accounts.google.com" 24 | # if remoteSchema or localSchema is http, you can ignore it 25 | remoteSchema: "https" 26 | local: "accounts.localhost.com" 27 | gstaticssl: 28 | remote: "ssl.gstatic.com" 29 | remoteSchema: "https" 30 | local: "gstaticssl.localhost.com" 31 | gstatic: 32 | remote: "www.gstatic.com" 33 | remoteSchema: "https" 34 | local: "gstatic.localhost.com" 35 | baidu: 36 | remote: "www.baidu.com" 37 | remoteSchema: "https" 38 | local: "baidu.localhost" -------------------------------------------------------------------------------- /filter/cookie.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/23/16 3 | package filter 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func FilterCookie(cookies []*http.Cookie, reverse bool) []*http.Cookie { 10 | for _, c := range cookies { 11 | replaceCookieDomain(c, reverse) 12 | } 13 | return cookies 14 | } 15 | 16 | func replaceCookieDomain(c *http.Cookie, reverse bool) { 17 | //if strings.HasPrefix(c.Domain, ".") { 18 | //if len(c.Domain) > 0 { 19 | if reverse { 20 | c.Domain = ".localhost.com" 21 | } else { 22 | c.Domain = ".google.com" 23 | } 24 | //} 25 | } 26 | -------------------------------------------------------------------------------- /filter/header.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/28/16 3 | package filter 4 | 5 | import ( 6 | "fmt" 7 | "github.com/kimiazhu/grp/model" 8 | "github.com/kimiazhu/grp/util/io" 9 | "github.com/kimiazhu/log4go" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | // 过滤src的Header信息, reverse=true表示将远程Host替换成本地Host。 15 | // 替换结果将直接放置于target中 16 | func FilterHeader(src, target http.Header, remoteHost string, isRequest bool) http.Header { 17 | for k, v := range src { 18 | for _, vv := range v { 19 | vv = ioutils.ReplaceHost(vv, isRequest) 20 | kk := strings.ToLower(k) 21 | if kk == "set-cookie" || kk == "cookie" { 22 | vv = DealCookie(remoteHost, vv, isRequest) 23 | } 24 | target.Add(k, vv) 25 | } 26 | } 27 | return target 28 | } 29 | 30 | // 处理Cookie中的域名信息, toHostName 表示这个cookie将要发送到的remoteHost 31 | // isRequest=true表示这个是代理服务器转发客户端或者浏览器的请求。反之表示此时是 32 | // 代理服务器转发服务端的应答 33 | func DealCookie(remoteHost, cookieData string, isRequest bool) string { 34 | cookies, err := ioutils.ParseRawCookies(cookieData) 35 | //log4go.Debug("@@@@@@@@@@@@@@@@@@@@@@@@@@%v", cookies) 36 | if err != nil { 37 | log4go.Error("parse cookie [%s] failed with error: %v\n", cookieData, err) 38 | panic(fmt.Sprintf("parse cookie failed: %s", cookieData)) 39 | } 40 | 41 | var newCookies string 42 | for i := 0; i < len(cookies); i++ { 43 | cookie := cookies[i] 44 | if topDomain := ioutils.TopDomainName(cookie.Domain); topDomain != "" { 45 | if !isRequest { 46 | cookie.Domain = model.LocalTopDomain 47 | } else { 48 | cookie.Domain = ioutils.TopDomainName(remoteHost) 49 | } 50 | } 51 | newCookies += (cookie.String() + ";") 52 | } 53 | 54 | if strings.HasSuffix(newCookies, ";") { 55 | newCookies = newCookies[:len(newCookies)-2] 56 | } 57 | 58 | return newCookies 59 | } 60 | -------------------------------------------------------------------------------- /filter/header_test.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/28/16 3 | package filter 4 | 5 | import "testing" 6 | 7 | func TestDealCookie(t *testing.T) { 8 | cookie := `NID=85=Sa5qdO8_-EADQ54Wz-DMgJLQ; expires=Mon, 27-Feb-2017 05:16:04 GMT; path=/; domain=.localhost.com; HttpOnly` 9 | t.Log(DealCookie("www.google.com", cookie, true)) 10 | } 11 | -------------------------------------------------------------------------------- /filter/request.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/23/16 3 | package filter 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "github.com/kimiazhu/grp/util/io" 8 | "github.com/kimiazhu/log4go" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | func CreateRequest(c *gin.Context, method, reqUrl, remoteHost string) *http.Request { 17 | req, _ := http.NewRequest(method, reqUrl, nil) 18 | 19 | contentLength := "" 20 | if method == "POST" { 21 | c.Request.ParseMultipartForm(32 << 20) //32M 22 | form := url.Values{} 23 | for k, v := range c.Request.PostForm { 24 | for _, vv := range v { 25 | form.Add(k, ioutils.ReplaceHost(vv, true)) 26 | } 27 | } 28 | 29 | encodedForm := form.Encode() 30 | req.Body = ioutil.NopCloser(strings.NewReader(encodedForm)) 31 | contentLength = strconv.Itoa(len([]byte(encodedForm))) 32 | } else { 33 | req.Body = c.Request.Body 34 | } 35 | 36 | FilterHeader(c.Request.Header, req.Header, remoteHost, true) 37 | req.Header.Add("Host", remoteHost) 38 | 39 | if cl := req.Header.Get("Content-Length"); len(cl) > 0 { 40 | log4go.Fine("old content length: %s, new content length: %s", cl, contentLength) 41 | req.Header.Set("Content-Length", contentLength) 42 | } 43 | 44 | req.Host = remoteHost 45 | return req 46 | } 47 | -------------------------------------------------------------------------------- /filter/response.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/15/16 3 | package filter 4 | 5 | import ( 6 | "bytes" 7 | "compress/gzip" 8 | "github.com/gin-gonic/gin" 9 | "github.com/kimiazhu/grp/util/io" 10 | "github.com/kimiazhu/log4go" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "net/http/httputil" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | var TEXT_CONTENT []string = []string{"text/plain", "text/html", "text/xml", "text/javascript", "text/json", 20 | "application/json", "application/xml", "application/javascript", "text/css"} 21 | 22 | func isText(contentType string) bool { 23 | for _, typ := range TEXT_CONTENT { 24 | if strings.Contains(contentType, typ) { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | func isZipped(encode string) bool { 32 | return strings.Contains(encode, "gzip") 33 | } 34 | 35 | func DumpResponse(resp *http.Response) { 36 | dat, e := httputil.DumpResponse(resp, true) 37 | if e != nil { 38 | log4go.Error("dump response failed: %v", e) 39 | } else { 40 | log4go.Error("dumped response body: %s", string(dat)) 41 | } 42 | } 43 | 44 | func renewContentLength(header http.Header, length int) { 45 | if header.Get("Content-Length") != "" { 46 | header.Set("Content-Length", strconv.Itoa(length)) 47 | } 48 | } 49 | 50 | func SmartRead(resp *http.Response, replaceHost bool) (body []byte, unzipped bool, err error) { 51 | encoding := resp.Header.Get("Content-Encoding") 52 | contentType := resp.Header.Get("Content-Type") 53 | if isText(contentType) && replaceHost { 54 | if isZipped(encoding) { 55 | // unzip the content 56 | var reader io.ReadCloser 57 | reader, err = gzip.NewReader(resp.Body) 58 | if err != nil { 59 | log4go.Error("new zip reader failed: %v", err) 60 | DumpResponse(resp) 61 | return 62 | } 63 | 64 | unzipped = true 65 | // read to byte array then replace all 66 | body, err = ioutil.ReadAll(reader) 67 | reader.Close() 68 | if err != nil { 69 | log4go.Error("read from zip reader failed: %v", err) 70 | return 71 | } 72 | } else { 73 | body, err = ioutil.ReadAll(resp.Body) 74 | if err != nil { 75 | log4go.Error("read from formal http body failed: %v", err) 76 | return 77 | } 78 | } 79 | 80 | data := ioutils.ReplaceHost(string(body), false) 81 | body = []byte(data) 82 | } else { 83 | // not compressed or not text(which can pass to client directly) 84 | body, err = ioutil.ReadAll(resp.Body) 85 | if err != nil { 86 | log4go.Error("read from reader failed: %v", err) 87 | return 88 | } 89 | } 90 | return 91 | } 92 | 93 | // resp是远程被代理的服务器返回的http应答。 94 | // body是我们处理过后的远程服务器应答Body数据。 95 | // unzipped 表示body数据是否经过解压,如果unzipped=true, 则表明解压过, 96 | // 那么在反馈给客户端之前需要重新压缩。 97 | func SmartWrite(c *gin.Context, resp *http.Response, body []byte, unzipped bool) { 98 | FilterHeader(resp.Header, c.Writer.Header(), "", false) 99 | 100 | newBody := body 101 | log4go.Fine("origin body length: %v, need compress: %v", len(body), unzipped) 102 | if len(body) > 0 && unzipped { 103 | var buf bytes.Buffer 104 | gz := gzip.NewWriter(&buf) 105 | _, err := gz.Write(body) 106 | gz.Close() 107 | if err != nil { 108 | log4go.Debug("zip body failed: %v", err) 109 | c.JSON(http.StatusInternalServerError, err.Error()) 110 | return 111 | } 112 | newBody = buf.Bytes() 113 | log4go.Fine("complete compress body, new length: %v", len(newBody)) 114 | } 115 | //writeCookies(c, resp, true) 116 | renewContentLength(c.Writer.Header(), len(newBody)) 117 | c.Writer.WriteHeader(resp.StatusCode) 118 | c.Writer.Write(newBody) 119 | c.Writer.Flush() 120 | } 121 | 122 | func writeCookies(c *gin.Context, resp *http.Response, reverse bool) { 123 | cookies := FilterCookie(resp.Cookies(), reverse) 124 | for _, cookie := range cookies { 125 | log4go.Debug("&&&&&&&&&&&&&&%s", cookie.Domain) 126 | log4go.Debug("&&&&&&&&&&&&&&%s", cookie.String()) 127 | //c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly) 128 | http.SetCookie(c.Writer, cookie) 129 | } 130 | } 131 | 132 | func ZipReader(encoding string, reader io.ReadCloser) (io.ReadCloser, error) { 133 | var r io.ReadCloser 134 | var err error 135 | switch encoding { 136 | case "gzip": 137 | r, err = gzip.NewReader(reader) 138 | if err != nil { 139 | return nil, err 140 | } 141 | default: 142 | r = reader 143 | } 144 | return r, nil 145 | } 146 | 147 | func ZipWriter(encoding string, writer io.Writer) io.Writer { 148 | var w io.Writer 149 | switch encoding { 150 | case "gzip": 151 | w = gzip.NewWriter(writer) 152 | default: 153 | w = writer 154 | } 155 | return w 156 | } 157 | -------------------------------------------------------------------------------- /grp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #================================================================= 4 | # GRP简单部署脚本。 5 | # 6 | # Author: ZHU HAIHUA 7 | # Version: 1.0 创建脚本。(2016-08-14) 8 | # Since: 2016-08-14 9 | #================================================================= 10 | 11 | FILENAME="./grp" 12 | PROC_NAME="grp" 13 | 14 | function restart() { 15 | echo "ready to restart" 16 | pgrep ${PROC_NAME} | xargs kill -9 17 | mv -f ${FILENAME} ${PROC_NAME} 18 | chmod a+x ${FILENAME} 19 | nohup ${FILENAME} > /dev/null 2>stderr.log & 20 | sleep 1 21 | echo "finish restart" 22 | pgrep ${PROC_NAME} 23 | } 24 | 25 | function reload() { 26 | echo "ready to reload" 27 | pgrep ${PROC_NAME} | xargs kill -HUP 28 | sleep 1 29 | echo "finish reload" 30 | pgrep ${PROC_NAME} 31 | } 32 | 33 | function kill() { 34 | echo "ready to kill grp" 35 | pgrep ${PROC_NAME} | xargs kill -9 36 | echo "finish" 37 | } 38 | 39 | function reload2() { 40 | echo "ready to reload" 41 | pids=`pgrep ${PROC_NAME}` 42 | for pid in $pids 43 | do 44 | #ssh -p$PORT $SERVER "kill -HUP $pid" 45 | echo "send hup signal to remote process: $pid" 46 | done 47 | } 48 | 49 | function usage() { 50 | IFS= 51 | usage_msg=" 52 | GRP deploy script. 53 | Userage: grp.sh [-h] [-r|-s|-k] 54 | -h, --help Show usage 55 | -r, --restart [grp_file] restart GRP, if exists, it will be rename to grp and then restart 56 | -s, --reload reload GRP 57 | -k, --kill stop current GRP service 58 | 59 | Examples: 60 | # stop grp, rename grp_2 to grp then restart 61 | $ ./grp.sh -r grp_2 62 | # reload grp 63 | $ ./grp.sh -s 64 | " 65 | echo $usage_msg 66 | IFS=, 67 | } 68 | 69 | #======================================================================= 70 | ########################### MAIN PROCCESS ############################## 71 | #======================================================================= 72 | ARGS=`getopt -o hr::sk --long help,restart::,reload,kill -n 'grp.sh' -- "$@"` 73 | if [ $# -eq 0 ]; then 74 | echo "Terminating... see -h or --help for help" 75 | exit 1 76 | fi 77 | eval set -- "${ARGS}" 78 | 79 | while [[ true ]]; do 80 | case "$1" in 81 | -h|--help) 82 | shift 83 | SHOW_HELP=yes 84 | ;; 85 | -r | --restart) 86 | RESTART=yes 87 | case $2 in 88 | "") 89 | echo "restart grp process" 90 | shift 2 91 | ;; 92 | *) 93 | FILENAME=$2 94 | echo "restart using ${FILENAME}" 95 | shift 2 96 | ;; 97 | esac 98 | ;; 99 | -s|--reload) 100 | shift 101 | RELOAD=yes 102 | ;; 103 | -k | --kill) 104 | shift 105 | KILL=yes 106 | ;; 107 | --) 108 | shift 109 | #break 110 | ;; 111 | -*) 112 | echo "Wrong option~~ Please read the flowing usage doc." 113 | echo "" 114 | usage 115 | exit 2 116 | ;; 117 | *) 118 | break 119 | ;; 120 | esac 121 | done 122 | 123 | if [ "$SHOW_HELP" = "yes" ]; then 124 | usage 125 | exit 0 126 | fi 127 | 128 | if [ -n "${RELOAD}" ]; then 129 | reload 130 | exit 0 131 | fi 132 | 133 | if [ -n "${RESTART}" ]; then 134 | restart 135 | exit 0 136 | fi 137 | 138 | if [ -n "${KILL}" ]; then 139 | kill 140 | exit 0 141 | fi 142 | -------------------------------------------------------------------------------- /log4go.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | stdout 4 | console 5 | ACCESS 6 | github.com/xgsdk2/betatest/tako.lib/mgox 7 | 8 | 9 | access 10 | file 11 | ACCESS 12 | log/access.log 13 | [%D %T] [%L] %M 14 | true 15 | 100M 16 | 100K 17 | true 18 | 19 | 20 | file_finest 21 | file 22 | 23 | FINEST 24 | github.com/yaosxi 25 | log/finest.log 26 | 37 | [%D %T] [%L] (%S) %M 38 | true 39 | 100M 40 | 100K 41 | true 42 | 43 | 44 | file_info 45 | file 46 | INFO 47 | log/info.log 48 | [%D %T] [%L] (%S) %M 49 | true 50 | 100M 51 | 100K 52 | true 53 | 54 | 55 | file_error 56 | file 57 | ERROR 58 | log/error.log 59 | [%D %T] [%L] (%S) %M 60 | true 61 | 100M 62 | 100K 63 | true 64 | 65 | 66 | xmllog 67 | xml 68 | TRACE 69 | trace.xml 70 | true 71 | 100M 72 | 100K 73 | false 74 | 75 | 76 | donotopen 77 | socket 78 | FINEST 79 | 192.168.1.255:12124 80 | udp 81 | 82 | 83 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/10/16 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "github.com/kimiazhu/ginweb" 8 | "github.com/kimiazhu/ginweb/conf" 9 | "github.com/kimiazhu/golib/utils" 10 | "github.com/kimiazhu/grp/midware" 11 | "github.com/kimiazhu/grp/model" 12 | "github.com/kimiazhu/grp/util/io" 13 | "github.com/kimiazhu/log4go" 14 | ) 15 | 16 | //var Servers []route.Server = make([]route.Server, 0) 17 | 18 | func init() { 19 | proxy := conf.Ext("proxy", map[interface{}]interface{}{}) 20 | for k, v := range proxy.(map[interface{}]interface{}) { 21 | log4go.Debug("found proxy config: %s", k.(string)) 22 | _v := v.(map[interface{}]interface{}) 23 | 24 | local := _v["local"].(string) 25 | localSchema := "http" 26 | if ls, ok := _v["localSchema"]; ok { 27 | localSchema = ls.(string) 28 | } 29 | local = combinePort(local, localSchema) 30 | model.SvrCnf[local] = &model.Server{Host: local, Schema: localSchema} 31 | 32 | remote := _v["remote"].(string) 33 | remoteSchema := "http" 34 | if rs, ok := _v["remoteSchema"]; ok { 35 | remoteSchema = rs.(string) 36 | } 37 | model.SvrCnf[remote] = &model.Server{Host: remote, Schema: remoteSchema} 38 | 39 | model.ReverseProxies[remote] = local 40 | model.Proxies[local] = remote 41 | 42 | if model.LocalTopDomain == "" { 43 | model.LocalTopDomain = ioutils.TopDomainName(local) 44 | } 45 | } 46 | log4go.Info("Load proxy list: %v", util.ReflectToString(model.Proxies)) 47 | } 48 | 49 | func combinePort(host, schema string) string { 50 | if (schema == "http" && conf.Conf.SERVER.PORT != "80") || 51 | schema == "https" && conf.Conf.SERVER.PORT != "443" { 52 | return fmt.Sprintf("%s:%s", host, conf.Conf.SERVER.PORT) 53 | } else { 54 | return host 55 | } 56 | } 57 | 58 | func main() { 59 | g := ginweb.New() 60 | g.Use(midware.Route()) 61 | ginweb.Run(conf.Conf.SERVER.PORT, g) 62 | } 63 | -------------------------------------------------------------------------------- /midware/router.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/10/16 3 | package midware 4 | 5 | import ( 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "github.com/kimiazhu/grp/filter" 9 | "github.com/kimiazhu/grp/model" 10 | "github.com/kimiazhu/grp/util/io" 11 | "github.com/kimiazhu/log4go" 12 | "net/http" 13 | "net/http/httputil" 14 | ) 15 | 16 | // Router 返回一个中间件函数, 将本地请求重定向至远端服务器, 17 | // 在拿到远端服务器应答之后, 替换应答中的远程服务器域名后将 18 | // 其回写到本地。 19 | func Route() func(*gin.Context) { 20 | return func(c *gin.Context) { 21 | local := c.Request.Host 22 | remote := model.Proxies[local] 23 | if remote == "" { 24 | log4go.Error("Local host [%s] cannot be found", local) 25 | msg := fmt.Errorf("no proxy for %s", local) 26 | c.AbortWithError(http.StatusNotFound, msg) 27 | c.String(http.StatusNotFound, msg.Error()) 28 | return 29 | } 30 | 31 | requestURI := ioutils.ReplaceHost(c.Request.RequestURI, true) 32 | reqUrl := fmt.Sprintf("%s://%s%s", model.SvrCnf[remote].Schema, remote, requestURI) 33 | method := c.Request.Method 34 | log4go.Fine("ready to request url: %s, method: %s", reqUrl, method) 35 | 36 | req := filter.CreateRequest(c, method, reqUrl, remote) 37 | resp, err := http.DefaultTransport.RoundTrip(req) 38 | 39 | if err != nil { 40 | log4go.Error("error occur while do request, error is: %v", err) 41 | dat, e := httputil.DumpRequestOut(req, true) 42 | if e != nil { 43 | log4go.Debug("dump out requst failed: %v", e) 44 | } else { 45 | log4go.Error("dumped request: %s", string(dat)) 46 | } 47 | c.AbortWithError(http.StatusInternalServerError, err) 48 | c.String(http.StatusInternalServerError, err.Error()) 49 | return 50 | } 51 | 52 | log4go.Debug("continue to contruct response of url: %s, httpStatus: %v", reqUrl, resp.Status) 53 | defer resp.Body.Close() 54 | 55 | body, unzipped, err := filter.SmartRead(resp, true) 56 | filter.SmartWrite(c, resp, body, unzipped) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/18/16 3 | package model 4 | 5 | import "fmt" 6 | 7 | type Server struct { 8 | Host string 9 | Schema string 10 | } 11 | 12 | func (s *Server) String() string { 13 | return fmt.Sprintf("%s://%s", s.Schema, s.Host) 14 | } 15 | 16 | // 反向代理是一个map对象,key是需要被代理的远程地址, 17 | // value是服务器本地地址,包括端口号 18 | type ReverseProxy map[string]string 19 | 20 | // 代理列表是一个map对象,key和value值和ReverseProxies 21 | // 正好相反 22 | type Proxy map[string]string 23 | 24 | // 服务器配置用于存储远程或者本地服务器的配置信息, 25 | // key 是服务器 Host 26 | // value 是Server对象指针 27 | type ServerConfig map[string]*Server 28 | 29 | var SvrCnf ServerConfig = make(ServerConfig) 30 | var ReverseProxies ReverseProxy = make(ReverseProxy) 31 | var Proxies Proxy = make(Proxy) 32 | var LocalTopDomain string = "" 33 | -------------------------------------------------------------------------------- /util/io/ioutil.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/23/16 3 | package ioutils 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "github.com/kimiazhu/grp/model" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // 替换请求中的Host, 在转发请求的时候, isRequest应该设置为true, 在转发应答的时候, reverse应该设置为false 14 | func ReplaceHost(data string, isRequest bool) string { 15 | for _local, _remote := range model.Proxies { 16 | if !isRequest { 17 | data = strings.Replace(data, model.SvrCnf[_remote].String(), model.SvrCnf[_local].String(), -1) 18 | } else { 19 | data = strings.Replace(data, model.SvrCnf[_local].String(), model.SvrCnf[_remote].String(), -1) 20 | } 21 | } 22 | return data 23 | } 24 | 25 | func ParseRawCookies(cookie string) ([]*http.Cookie, error) { 26 | req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(fmt.Sprintf("GET / HTTP/1.0\r\nCookie: %s\r\n\r\n", cookie)))) 27 | if err != nil { 28 | return nil, err 29 | } 30 | cookies := req.Cookies() 31 | if len(cookies) == 0 { 32 | return nil, fmt.Errorf("no cookies") 33 | } 34 | return cookies, nil 35 | } 36 | 37 | func IsTopDomain(domain string) bool { 38 | if strings.Count(domain, ".") == 1 || (strings.HasPrefix(domain, ".") && strings.Count(domain, ".") == 2) { 39 | return true 40 | } 41 | return false 42 | } 43 | 44 | func TopDomainName(domain string) string { 45 | if strings.HasPrefix(domain, ".") { 46 | domain = domain[1:] 47 | } 48 | 49 | if ci := strings.LastIndex(domain, ":"); ci > 0 { 50 | domain = domain[:ci] 51 | } 52 | 53 | if strings.Count(domain, ".") == 1 { 54 | return domain 55 | } 56 | 57 | ss := strings.Split(domain, ".") 58 | length := len(ss) 59 | if length < 2 { 60 | return "" 61 | } else { 62 | return strings.Join(ss[length-2:], ".") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /util/io/ioutil_test.go: -------------------------------------------------------------------------------- 1 | // Author: ZHU HAIHUA 2 | // Date: 8/28/16 3 | package ioutils 4 | 5 | import "testing" 6 | 7 | func TestTopDomainName(t *testing.T) { 8 | t.Log(TopDomainName("g.localhost.com")) 9 | t.Log(TopDomainName(".localhost.com")) 10 | t.Log(TopDomainName(".g.localhost.com")) 11 | t.Log(TopDomainName(".com")) 12 | t.Log(TopDomainName("gg.localhost.com:8888")) 13 | } 14 | --------------------------------------------------------------------------------