diff --git a/CHANGELOG b/CHANGELOG index 3fe334e..7f20521 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ proxy更新日志 +v3.6 +1.http(s),socks代理,集成了外部HTTP API认证,可以通过外部API对用户名和密码进行认证. +2.手册http(s),socks代理认证部分增加了集成外部HTTP API认证的使用说明. + v3.5 1.优化了kcp参数,速度有所提升. 2.修复了socks无法正常工作的问题. diff --git a/README.md b/README.md index fe1182b..e036578 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Proxy是golang实现的高性能http,https,websocket,tcp,udp,socks5代理服务 - ...   -本页是v3.5手册,其他版本手册请点击下面链接查看. +本页是v3.6手册,其他版本手册请点击下面链接查看. +- [v3.5手册](https://github.com/snail007/goproxy/tree/v3.5) - [v3.4手册](https://github.com/snail007/goproxy/tree/v3.4) - [v3.3手册](https://github.com/snail007/goproxy/tree/v3.3) - [v3.2手册](https://github.com/snail007/goproxy/tree/v3.2) @@ -118,7 +119,7 @@ wget https://github.com/reddec/monexec/releases/download/v0.1.1/monexec_0.1.1_li 下载地址:https://github.com/snail007/goproxy/releases ```shell cd /root/proxy/ -wget https://github.com/snail007/goproxy/releases/download/v3.5/proxy-linux-amd64.tar.gz +wget https://github.com/snail007/goproxy/releases/download/v3.6/proxy-linux-amd64.tar.gz ``` #### **3.下载自动安装脚本** ```shell @@ -203,9 +204,23 @@ http,tcp,udp代理过程会和上级通讯,为了安全我们采用加密通讯, `./proxy http -t tcp -p ":33080" -a "user1:pass1" -a "user2:pass2"` 多个用户,重复-a参数即可. 也可以放在文件中,格式是一行一个"用户名:密码",然后用-F指定. -`./proxy http -t tcp -p ":33080" -F auth-file.txt` -如果没有-a或-F参数,就是关闭Basic认证. +`./proxy http -t tcp -p ":33080" -F auth-file.txt` +另外,http(s)代理还集成了外部HTTP API认证,我们可以通过--auth-url参数指定一个http url接口地址, +然后有用户连接的时候,proxy会GET方式请求这url,带上下面四个参数,如果返回HTTP状态码204,代表认证成功 +其它情况认为认证失败. +比如: +`./proxy http -t tcp -p ":33080" --auth-url "http://test.com/auth.php"` +用户连接的时候,proxy会GET方式请求这url("http://test.com/auth.php"), +带上user,pass,ip,target四个参数: +http://test.com/auth.php?user={USER}&pass={PASS}&ip={IP}&target={TARGET} +user:用户名 +pass:密码 +ip:用户的IP,比如:192.168.1.200 +target:用户访问的URL,比如:http://demo.com:80/1.html或https://www.baidu.com:80 + +如果没有-a或-F或--auth-url参数,就是关闭Basic认证. + #### **1.6.HTTP代理流量强制走上级HTTP代理** 默认情况下,proxy会智能判断一个网站域名是否无法访问,如果无法访问才走上级HTTP代理.通过--always可以使全部HTTP代理流量强制走上级HTTP代理. `./proxy http --always -t tls -p ":28080" -T tls -P "22.22.22.22:38080" -C proxy.crt -K proxy.key` @@ -511,7 +526,20 @@ server连接到bridge的时候,如果同时有多个client连接到同一个brid 多个用户,重复-a参数即可. 也可以放在文件中,格式是一行一个"用户名:密码",然后用-F指定. `./proxy socks -t tcp -p ":33080" -F auth-file.txt` -如果没有-a或-F参数,就是关闭认证. + +另外,socks5代理还集成了外部HTTP API认证,我们可以通过--auth-url参数指定一个http url接口地址, +然后有用户连接的时候,proxy会GET方式请求这url,带上下面四个参数,如果返回HTTP状态码204,代表认证成功 +其它情况认为认证失败. +比如: +`./proxy socks -t tcp -p ":33080" --auth-url "http://test.com/auth.php"` +用户连接的时候,proxy会GET方式请求这url("http://test.com/auth.php"), +带上user,pass,ip,三个参数: +http://test.com/auth.php?user={USER}&pass={PASS}&ip={IP} +user:用户名 +pass:密码 +ip:用户的IP,比如:192.168.1.200 + +如果没有-a或-F或--auth-url参数,就是关闭认证. #### **5.8.KCP协议传输** KCP协议需要-B参数设置一个密码用于加密解密数据 diff --git a/config.go b/config.go index 07bcbf9..0c9d939 100755 --- a/config.go +++ b/config.go @@ -63,6 +63,10 @@ func initConfig() (err error) { httpArgs.KCPKey = http.Flag("kcp-key", "key for kcp encrypt/decrypt data").Short('B').Default("encrypt").String() httpArgs.KCPMethod = http.Flag("kcp-method", "kcp encrypt/decrypt method").Short('M').Default("3des").String() httpArgs.LocalIPS = http.Flag("local bind ips", "if your host behind a nat,set your public ip here avoid dead loop").Short('g').Strings() + httpArgs.AuthURL = http.Flag("auth-url", "http basic auth username and password will send to this url,response http code equal to 'auth-code' means ok,others means fail.").Default("").String() + httpArgs.AuthURLTimeout = http.Flag("auth-timeout", "access 'auth-url' timeout milliseconds").Default("3000").Int() + httpArgs.AuthURLOkCode = http.Flag("auth-code", "access 'auth-url' success http code").Default("204").Int() + httpArgs.AuthURLRetry = http.Flag("auth-retry", "access 'auth-url' fail and retry count").Default("1").Int() //########tcp######### tcp := app.Command("tcp", "proxy on tcp mode") @@ -138,6 +142,10 @@ func initConfig() (err error) { socksArgs.KCPKey = socks.Flag("kcp-key", "key for kcp encrypt/decrypt data").Short('B').Default("encrypt").String() socksArgs.KCPMethod = socks.Flag("kcp-method", "kcp encrypt/decrypt method").Short('M').Default("3des").String() socksArgs.LocalIPS = socks.Flag("local bind ips", "if your host behind a nat,set your public ip here avoid dead loop").Short('g').Strings() + socksArgs.AuthURL = socks.Flag("auth-url", "auth username and password will send to this url,response http code equal to 'auth-code' means ok,others means fail.").Default("").String() + socksArgs.AuthURLTimeout = socks.Flag("auth-timeout", "access 'auth-url' timeout milliseconds").Default("3000").Int() + socksArgs.AuthURLOkCode = socks.Flag("auth-code", "access 'auth-url' success http code").Default("204").Int() + socksArgs.AuthURLRetry = socks.Flag("auth-retry", "access 'auth-url' fail and retry count").Default("0").Int() //parse args serviceName := kingpin.MustParse(app.Parse(os.Args[1:])) diff --git a/install_auto.sh b/install_auto.sh index 6823b15..d29444d 100755 --- a/install_auto.sh +++ b/install_auto.sh @@ -6,7 +6,7 @@ fi mkdir /tmp/proxy cd /tmp/proxy wget https://github.com/reddec/monexec/releases/download/v0.1.1/monexec_0.1.1_linux_amd64.tar.gz -wget https://github.com/snail007/goproxy/releases/download/v3.5/proxy-linux-amd64.tar.gz +wget https://github.com/snail007/goproxy/releases/download/v3.6/proxy-linux-amd64.tar.gz # install monexec tar zxvf monexec_0.1.1_linux_amd64.tar.gz diff --git a/main.go b/main.go index 9da43dd..5e47893 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "syscall" ) -const APP_VERSION = "3.5" +const APP_VERSION = "3.6" func main() { err := initConfig() diff --git a/release.sh b/release.sh index 21447cb..ac3eae4 100755 --- a/release.sh +++ b/release.sh @@ -1,5 +1,5 @@ #!/bin/bash -VER="3.5" +VER="3.6" RELEASE="release-${VER}" rm -rf .cert mkdir .cert diff --git a/services/args.go b/services/args.go index c448fc9..100d23c 100644 --- a/services/args.go +++ b/services/args.go @@ -80,6 +80,10 @@ type HTTPArgs struct { Direct *string AuthFile *string Auth *[]string + AuthURL *string + AuthURLOkCode *int + AuthURLTimeout *int + AuthURLRetry *int ParentType *string LocalType *string Timeout *int @@ -129,6 +133,10 @@ type SocksArgs struct { Direct *string AuthFile *string Auth *[]string + AuthURL *string + AuthURLOkCode *int + AuthURLTimeout *int + AuthURLRetry *int KCPMethod *string KCPKey *string UDPParent *string diff --git a/services/http.go b/services/http.go index 0a3fc3d..73b6cdc 100644 --- a/services/http.go +++ b/services/http.go @@ -146,7 +146,7 @@ func (s *HTTP) callback(inConn net.Conn) { req, err = utils.NewHTTPRequest(&inConn, 4096, s.IsBasicAuth(), &s.basicAuth) if err != nil { if err != io.EOF { - log.Printf("decoder error , form %s, ERR:%s", err, inConn.RemoteAddr()) + log.Printf("decoder error , from %s, ERR:%s", inConn.RemoteAddr(), err) } utils.CloseConn(&inConn) return @@ -322,6 +322,10 @@ func (s *HTTP) InitOutConnPool() { } func (s *HTTP) InitBasicAuth() (err error) { s.basicAuth = utils.NewBasicAuth() + if *s.cfg.AuthURL != "" { + s.basicAuth.SetAuthURL(*s.cfg.AuthURL, *s.cfg.AuthURLOkCode, *s.cfg.AuthURLTimeout, *s.cfg.AuthURLRetry) + log.Printf("auth from %s", *s.cfg.AuthURL) + } if *s.cfg.AuthFile != "" { var n = 0 n, err = s.basicAuth.AddFromFile(*s.cfg.AuthFile) @@ -338,7 +342,7 @@ func (s *HTTP) InitBasicAuth() (err error) { return } func (s *HTTP) IsBasicAuth() bool { - return *s.cfg.AuthFile != "" || len(*s.cfg.Auth) > 0 + return *s.cfg.AuthFile != "" || len(*s.cfg.Auth) > 0 || *s.cfg.AuthURL != "" } func (s *HTTP) IsDeadLoop(inLocalAddr string, host string) bool { inIP, inPort, err := net.SplitHostPort(inLocalAddr) diff --git a/services/socks.go b/services/socks.go index 3ce80dd..9953666 100644 --- a/services/socks.go +++ b/services/socks.go @@ -10,6 +10,7 @@ import ( "proxy/utils/aes" "proxy/utils/socks" "runtime/debug" + "strings" "time" "golang.org/x/crypto/ssh" @@ -353,7 +354,8 @@ func (s *Socks) socksConnCallback(inConn net.Conn) { pass := string(r[2+r[1]+1:]) //log.Printf("user:%s,pass:%s", user, pass) //auth - if s.basicAuth.CheckUserPass(user, pass) { + _addr := strings.Split(inConn.RemoteAddr().String(), ":") + if s.basicAuth.CheckUserPass(user, pass, _addr[0], "") { inConn.Write([]byte{0x01, 0x00}) } else { inConn.Write([]byte{0x01, 0x01}) @@ -560,6 +562,10 @@ func (s *Socks) ConnectSSH() (err error) { } func (s *Socks) InitBasicAuth() (err error) { s.basicAuth = utils.NewBasicAuth() + if *s.cfg.AuthURL != "" { + s.basicAuth.SetAuthURL(*s.cfg.AuthURL, *s.cfg.AuthURLOkCode, *s.cfg.AuthURLTimeout, *s.cfg.AuthURLRetry) + log.Printf("auth from %s", *s.cfg.AuthURL) + } if *s.cfg.AuthFile != "" { var n = 0 n, err = s.basicAuth.AddFromFile(*s.cfg.AuthFile) @@ -576,7 +582,7 @@ func (s *Socks) InitBasicAuth() (err error) { return } func (s *Socks) IsBasicAuth() bool { - return *s.cfg.AuthFile != "" || len(*s.cfg.Auth) > 0 + return *s.cfg.AuthFile != "" || len(*s.cfg.Auth) > 0 || *s.cfg.AuthURL != "" } func (s *Socks) IsDeadLoop(inLocalAddr string, host string) bool { inIP, inPort, err := net.SplitHostPort(inLocalAddr) diff --git a/utils/functions.go b/utils/functions.go index 9842dd6..6718002 100755 --- a/utils/functions.go +++ b/utils/functions.go @@ -428,6 +428,29 @@ func GetKCPBlock(method, key string) (block kcp.BlockCrypt) { } return } +func HttpGet(URL string, timeout int) (body []byte, code int, err error) { + var tr *http.Transport + var client *http.Client + conf := &tls.Config{ + InsecureSkipVerify: true, + } + if strings.Contains(URL, "https://") { + tr = &http.Transport{TLSClientConfig: conf} + client = &http.Client{Timeout: time.Millisecond * time.Duration(timeout), Transport: tr} + } else { + tr = &http.Transport{} + client = &http.Client{Timeout: time.Millisecond * time.Duration(timeout), Transport: tr} + } + defer tr.CloseIdleConnections() + resp, err := client.Get(URL) + if err != nil { + return + } + defer resp.Body.Close() + code = resp.StatusCode + body, err = ioutil.ReadAll(resp.Body) + return +} // type sockaddr struct { // family uint16 diff --git a/utils/structs.go b/utils/structs.go index a64286f..9e5fe9e 100644 --- a/utils/structs.go +++ b/utils/structs.go @@ -176,7 +176,11 @@ func (c *Checker) Add(address string, isHTTPS bool, method, URL string, data []b } type BasicAuth struct { - data ConcurrentMap + data ConcurrentMap + authURL string + authOkCode int + authTimeout int + authRetry int } func NewBasicAuth() BasicAuth { @@ -184,6 +188,12 @@ func NewBasicAuth() BasicAuth { data: NewConcurrentMap(), } } +func (ba *BasicAuth) SetAuthURL(URL string, code, timeout, retry int) { + ba.authURL = URL + ba.authOkCode = code + ba.authTimeout = timeout + ba.authRetry = retry +} func (ba *BasicAuth) AddFromFile(file string) (n int, err error) { _content, err := ioutil.ReadFile(file) if err != nil { @@ -213,21 +223,69 @@ func (ba *BasicAuth) Add(userpassArr []string) (n int) { } return } -func (ba *BasicAuth) CheckUserPass(user, pass string) (ok bool) { - if p, _ok := ba.data.Get(user); _ok { - return p.(string) == pass - } - return +func (ba *BasicAuth) CheckUserPass(user, pass, ip, target string) (ok bool) { + + return ba.Check(user+":"+pass, ip, target) } -func (ba *BasicAuth) Check(userpass string) (ok bool) { +func (ba *BasicAuth) Check(userpass string, ip, target string) (ok bool) { u := strings.Split(strings.Trim(userpass, " "), ":") if len(u) == 2 { if p, _ok := ba.data.Get(u[0]); _ok { return p.(string) == u[1] } + if ba.authURL != "" { + err := ba.checkFromURL(userpass, ip, target) + if err == nil { + return true + } + log.Printf("%s", err) + } + return false } return } +func (ba *BasicAuth) checkFromURL(userpass, ip, target string) (err error) { + u := strings.Split(strings.Trim(userpass, " "), ":") + if len(u) != 2 { + return + } + URL := ba.authURL + if strings.Contains(URL, "?") { + URL += "&" + } else { + URL += "?" + } + URL += fmt.Sprintf("user=%s&pass=%s&ip=%s&target=%s", u[0], u[1], ip, target) + var code int + var tryCount = 0 + var body []byte + for tryCount <= ba.authRetry { + body, code, err = HttpGet(URL, ba.authTimeout) + if err == nil && code == ba.authOkCode { + break + } else if err != nil { + err = fmt.Errorf("auth fail from url %s,resonse err:%s , %s", URL, err, ip) + } else { + if len(body) > 0 { + err = fmt.Errorf(string(body[0:100])) + } else { + err = fmt.Errorf("token error") + } + err = fmt.Errorf("auth fail from url %s,resonse code: %d, except: %d , %s , %s", URL, code, ba.authOkCode, ip, string(body)) + } + if err != nil && tryCount < ba.authRetry { + log.Print(err) + time.Sleep(time.Second * 2) + } + tryCount++ + } + if err != nil { + return + } + //log.Printf("auth success from auth url, %s", ip) + return +} + func (ba *BasicAuth) Total() (n int) { n = ba.data.Count() return @@ -292,7 +350,11 @@ func (req *HTTPRequest) HTTP() (err error) { } req.URL, err = req.getHTTPURL() if err == nil { - u, _ := url.Parse(req.URL) + var u *url.URL + u, err = url.Parse(req.URL) + if err != nil { + return + } req.Host = u.Host req.addPortIfNot() } @@ -321,6 +383,14 @@ func (req *HTTPRequest) BasicAuth() (err error) { CloseConn(req.conn) return } + if authorization == "" { + authorization, err = req.getHeader("Proxy-Authorization") + if err != nil { + fmt.Fprint((*req.conn), "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"\"\r\n\r\nUnauthorized") + CloseConn(req.conn) + return + } + } //log.Printf("Authorization:%s", authorization) basic := strings.Fields(authorization) if len(basic) != 2 { @@ -334,7 +404,14 @@ func (req *HTTPRequest) BasicAuth() (err error) { CloseConn(req.conn) return } - authOk := (*req.basicAuth).Check(string(user)) + addr := strings.Split((*req.conn).RemoteAddr().String(), ":") + URL := "" + if req.IsHTTPS() { + URL = "https://" + req.Host + } else { + URL, _ = req.getHTTPURL() + } + authOk := (*req.basicAuth).Check(string(user), addr[0], URL) //log.Printf("auth %s,%v", string(user), authOk) if !authOk { fmt.Fprint((*req.conn), "HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized") @@ -358,6 +435,7 @@ func (req *HTTPRequest) getHTTPURL() (URL string, err error) { func (req *HTTPRequest) getHeader(key string) (val string, err error) { key = strings.ToUpper(key) lines := strings.Split(string(req.HeadBuf), "\r\n") + //log.Println(lines) for _, line := range lines { line := strings.SplitN(strings.Trim(line, "\r\n "), ":", 2) if len(line) == 2 { @@ -369,7 +447,6 @@ func (req *HTTPRequest) getHeader(key string) (val string, err error) { } } } - err = fmt.Errorf("can not find HOST header") return }