diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9267d611 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +Dockerfile +.git +*~ +*# +.#* diff --git a/.travis.yml b/.travis.yml index 89a8b00d..3ccb28ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: go go: - 1.5.4 - 1.6.3 - - 1.7rc6 + - 1.7 install: - make diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..934e9913 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.6 + +COPY . /go/src/github.com/fatedier/frp + +RUN cd /go/src/github.com/fatedier/frp \ + && make \ + && mv bin/frpc /frpc \ + && mv bin/frps /frps \ + && mv conf/frpc_min.ini /frpc.ini \ + && mv conf/frps_min.ini /frps.ini \ + && make clean + +WORKDIR / + +EXPOSE 80 443 6000 7000 7500 + +ENTRYPOINT ["/frps"] diff --git a/Dockerfile_alpine b/Dockerfile_alpine new file mode 100644 index 00000000..671b563e --- /dev/null +++ b/Dockerfile_alpine @@ -0,0 +1,12 @@ +FROM alpine:3.4 + +COPY bin/frpc /frpc +COPY bin/frps /frps +COPY conf/frpc_min.ini /frpc.ini +COPY conf/frps_min.ini /frps.ini + +WORKDIR / + +EXPOSE 80 443 6000 7000 7500 + +ENTRYPOINT ["/frps"] diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index 2cb5ab24..5f4daf69 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -3,10 +3,16 @@ export GO15VENDOREXPERIMENT := 1 all: build -build: gox app +build: gox app more gox: go get github.com/mitchellh/gox app: gox -osarch "darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386 windows/amd64" ./src/... + +more: + env GOOS=linux GOARCH=mips64 go build -o ./frpc_linux_mips64 ./src/cmd/frpc + env GOOS=linux GOARCH=mips64 go build -o ./frps_linux_mips64 ./src/cmd/frps + env GOOS=linux GOARCH=mips64le go build -o ./frpc_linux_mips64le ./src/cmd/frpc + env GOOS=linux GOARCH=mips64le go build -o ./frps_linux_mips64le ./src/cmd/frps diff --git a/README.md b/README.md index 941dd1d8..205e1532 100644 --- a/README.md +++ b/README.md @@ -150,9 +150,12 @@ Configure a port for dashboard to enable this feature: ```ini [common] dashboard_port = 7500 +# dashboard's username and password are both optional,if not set, default is admin. +dashboard_username = abc +dashboard_password = abc ``` -Then visit `http://[server_addr]:7500` to see dashboard. +Then visit `http://[server_addr]:7500` to see dashboard, default username and password are both `admin`. ![dashboard](/doc/pic/dashboard.png) @@ -327,7 +330,9 @@ If `host_header_rewrite` is specified, the Host header will be rewritten to matc Interested in getting involved? We would like to help you! -* Take a look at our [issues list](https://github.com/fatedier/frp/issues) and consider submitting a patch +* Take a look at our [issues list](https://github.com/fatedier/frp/issues) and consider sending a Pull Request to **dev branch**. +* If you want to add a new feature, please create an issue first to describe the new feature, as well as the implementation approach. Once a proposal is accepted, create an implementation of the new features and submit it as a pull request. +* Sorry for my poor english and improvement for this document is welcome even some typo fix. * If you have some wanderful ideas, send email to fatedier@gmail.com. **Note: We prefer you to give your advise in [issues](https://github.com/fatedier/frp/issues), so others with a same question can search it quickly and we don't need to answer them repeatly.** @@ -348,5 +353,8 @@ Donate money by [paypal](https://www.paypal.me/fatedier) to my account **fatedie * [fatedier](https://github.com/fatedier) * [Hurricanezwf](https://github.com/Hurricanezwf) -* [vashstorm](https://github.com/vashstorm) -* [maodanp](https://github.com/maodanp) +* [Pan Hao](https://github.com/vashstorm) +* [Danping Mao](https://github.com/maodanp) +* [Eric Larssen](https://github.com/ericlarssen) +* [Damon Zhao](https://github.com/se77en) +* [Manfred Touron](https://github.com/moul) diff --git a/README_zh.md b/README_zh.md index 16928fe9..6f389f2a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -24,7 +24,7 @@ frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内 * [连接池](#连接池) * [修改 Host Header](#修改-host-header) * [开发计划](#开发计划) -* [贡献代码](#贡献代码) +* [为 frp 做贡献](#为-frp-做贡献) * [捐助](#捐助) * [贡献者](#贡献者) @@ -147,9 +147,12 @@ frp 目前正在前期开发阶段,master 分支用于发布稳定版本,dev ```ini [common] dashboard_port = 7500 +# dashboard 用户名密码可选,默认都为 admin +dashboard_username = abc +dashboard_password = abc ``` -打开浏览器通过 `http://[server_addr]:7500` 访问 dashboard 界面。 +打开浏览器通过 `http://[server_addr]:7500` 访问 dashboard 界面,用户名密码默认为 `admin`。 ![dashboard](/doc/pic/dashboard.png) @@ -330,12 +333,16 @@ host_header_rewrite = dev.yourdomain.com * frpc 完全控制模式,通过 dashboard 对 frpc 进行在线操作。 * 支持 udp 打洞的方式,提供两边内网机器直接通信,流量不经过服务器转发。 -## 贡献代码 +## 为 frp 做贡献 -如果您对这个项目感兴趣,我们非常欢迎您参与其中! +frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进步贡献力量。 -* 如果您需要提交问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来完成。 -* 如果您有新的功能需求,可以反馈至 fatedier@gmail.com 共同讨论。 +* 在使用过程中出现任何问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来反馈。 +* Bug 的修复可以直接提交 Pull Request 到 dev 分支。 +* 如果是增加新的功能特性,请先创建一个 issue 并做简单描述以及大致的实现方法,提议被采纳后,就可以创建一个实现新特性的 Pull Request。 +* 欢迎对说明文档做出改善,帮助更多的人使用 frp,特别是英文文档。 +* 贡献代码请提交 PR 至 dev 分支,master 分支仅用于发布稳定可用版本。 +* 如果你有任何其他方面的问题,欢迎反馈至 fatedier@gmail.com 共同交流。 **提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。** @@ -355,5 +362,8 @@ host_header_rewrite = dev.yourdomain.com * [fatedier](https://github.com/fatedier) * [Hurricanezwf](https://github.com/Hurricanezwf) -* [vashstorm](https://github.com/vashstorm) -* [maodanp](https://github.com/maodanp) +* [Pan Hao](https://github.com/vashstorm) +* [Danping Mao](https://github.com/maodanp) +* [Eric Larssen](https://github.com/ericlarssen) +* [Damon Zhao](https://github.com/se77en) +* [Manfred Touron](https://github.com/moul) diff --git a/conf/frpc.ini b/conf/frpc.ini index 9e2030ac..98517708 100644 --- a/conf/frpc.ini +++ b/conf/frpc.ini @@ -4,6 +4,8 @@ # in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" server_addr = 0.0.0.0 server_port = 7000 +# if you want to connect frps by http proxy, you can set http_proxy here or in global environment variables +# http_proxy = http://user:pwd@192.168.1.128:8080 # console or real logFile path like ./frpc.log log_file = ./frpc.log # debug, info, warn, error diff --git a/conf/frpc_min.ini b/conf/frpc_min.ini new file mode 100644 index 00000000..a49901d9 --- /dev/null +++ b/conf/frpc_min.ini @@ -0,0 +1,10 @@ +[common] +server_addr = 0.0.0.0 +server_port = 7000 +auth_token = 123 +privilege_token = 12345678 + +[ssh] +type = tcp +local_ip = 127.0.0.1 +local_port = 22 diff --git a/conf/frps.ini b/conf/frps.ini index 4a6d47f8..6163bc98 100644 --- a/conf/frps.ini +++ b/conf/frps.ini @@ -9,6 +9,9 @@ vhost_http_port = 80 vhost_https_port = 443 # if you want to configure or reload frps by dashboard, dashboard_port must be set dashboard_port = 7500 +# dashboard username and password for basic protect, if not set, both default value is admin +dashboard_username = abc +dashboard_password = abc # dashboard assets directory(only for debug mode) # assets_dir = ./static # console or real logFile path like ./frps.log diff --git a/conf/frps_min.ini b/conf/frps_min.ini new file mode 100644 index 00000000..a0df74ad --- /dev/null +++ b/conf/frps_min.ini @@ -0,0 +1,14 @@ +[common] +bind_addr = 0.0.0.0 +bind_port = 7000 +vhost_http_port = 80 +vhost_https_port = 443 +dashboard_port = 7500 +privilege_mode = true +privilege_token = 12345678 + +[ssh] +type = tcp +auth_token = 123 +bind_addr = 0.0.0.0 +listen_port = 6000 diff --git a/cross_compiles_package.sh b/package.sh similarity index 97% rename from cross_compiles_package.sh rename to package.sh index 5cbe6a67..9b73e93e 100755 --- a/cross_compiles_package.sh +++ b/package.sh @@ -15,7 +15,7 @@ rm -rf ./packages mkdir ./packages os_all='linux windows darwin' -arch_all='386 amd64 arm' +arch_all='386 amd64 arm mips64 mips64le' for os in $os_all; do for arch in $arch_all; do diff --git a/src/cmd/frpc/control.go b/src/cmd/frpc/control.go index 3fdb611c..d39a45d4 100644 --- a/src/cmd/frpc/control.go +++ b/src/cmd/frpc/control.go @@ -130,7 +130,11 @@ func msgSender(cli *client.ProxyClient, c *conn.Conn, msgSendChan chan interface } func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) { - c, err = conn.ConnectServer(client.ServerAddr, client.ServerPort) + if client.HttpProxy == "" { + c, err = conn.ConnectServer(fmt.Sprintf("%s:%d", client.ServerAddr, client.ServerPort)) + } else { + c, err = conn.ConnectServerByHttpProxy(client.HttpProxy, fmt.Sprintf("%s:%d", client.ServerAddr, client.ServerPort)) + } if err != nil { log.Error("ProxyName [%s], connect to server [%s:%d] error, %v", cli.Name, client.ServerAddr, client.ServerPort, err) return @@ -144,6 +148,7 @@ func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) { UseGzip: cli.UseGzip, PrivilegeMode: cli.PrivilegeMode, ProxyType: cli.Type, + PoolCount: cli.PoolCount, HostHeaderRewrite: cli.HostHeaderRewrite, Timestamp: nowTime, } diff --git a/src/models/client/client.go b/src/models/client/client.go index 5c82fbb4..2548b237 100644 --- a/src/models/client/client.go +++ b/src/models/client/client.go @@ -37,7 +37,7 @@ type ProxyClient struct { } func (p *ProxyClient) GetLocalConn() (c *conn.Conn, err error) { - c, err = conn.ConnectServer(p.LocalIp, p.LocalPort) + c, err = conn.ConnectServer(fmt.Sprintf("%s:%d", p.LocalIp, p.LocalPort)) if err != nil { log.Error("ProxyName [%s], connect to local port error, %v", p.Name, err) } @@ -51,7 +51,11 @@ func (p *ProxyClient) GetRemoteConn(addr string, port int64) (c *conn.Conn, err } }() - c, err = conn.ConnectServer(addr, port) + if HttpProxy == "" { + c, err = conn.ConnectServer(fmt.Sprintf("%s:%d", addr, port)) + } else { + c, err = conn.ConnectServerByHttpProxy(HttpProxy, fmt.Sprintf("%s:%d", addr, port)) + } if err != nil { log.Error("ProxyName [%s], connect to server [%s:%d] error, %v", p.Name, addr, port, err) return diff --git a/src/models/client/config.go b/src/models/client/config.go index 3068ef17..ddc5d1d4 100644 --- a/src/models/client/config.go +++ b/src/models/client/config.go @@ -16,6 +16,7 @@ package client import ( "fmt" + "os" "strconv" "strings" @@ -26,6 +27,7 @@ import ( var ( ServerAddr string = "0.0.0.0" ServerPort int64 = 7000 + HttpProxy string = "" LogFile string = "console" LogWay string = "console" LogLevel string = "info" @@ -57,6 +59,14 @@ func LoadConf(confFile string) (err error) { ServerPort, _ = strconv.ParseInt(tmpStr, 10, 64) } + tmpStr, ok = conf.Get("common", "http_proxy") + if ok { + HttpProxy = tmpStr + } else { + // get http_proxy from env + HttpProxy = os.Getenv("http_proxy") + } + tmpStr, ok = conf.Get("common", "log_file") if ok { LogFile = tmpStr diff --git a/src/models/server/config.go b/src/models/server/config.go index e3296d05..58ae0da3 100644 --- a/src/models/server/config.go +++ b/src/models/server/config.go @@ -30,19 +30,21 @@ import ( // common config var ( - ConfigFile string = "./frps.ini" - BindAddr string = "0.0.0.0" - BindPort int64 = 7000 - VhostHttpPort int64 = 0 // if VhostHttpPort equals 0, don't listen a public port for http protocol - VhostHttpsPort int64 = 0 // if VhostHttpsPort equals 0, don't listen a public port for https protocol - DashboardPort int64 = 0 // if DashboardPort equals 0, dashboard is not available - AssetsDir string = "" - LogFile string = "console" - LogWay string = "console" // console or file - LogLevel string = "info" - LogMaxDays int64 = 3 - PrivilegeMode bool = false - PrivilegeToken string = "" + ConfigFile string = "./frps.ini" + BindAddr string = "0.0.0.0" + BindPort int64 = 7000 + VhostHttpPort int64 = 0 // if VhostHttpPort equals 0, don't listen a public port for http protocol + VhostHttpsPort int64 = 0 // if VhostHttpsPort equals 0, don't listen a public port for https protocol + DashboardPort int64 = 0 // if DashboardPort equals 0, dashboard is not available + DashboardUsername string = "admin" + DashboardPassword string = "admin" + AssetsDir string = "" + LogFile string = "console" + LogWay string = "console" // console or file + LogLevel string = "info" + LogMaxDays int64 = 3 + PrivilegeMode bool = false + PrivilegeToken string = "" // if PrivilegeAllowPorts is not nil, tcp proxies which remote port exist in this map can be connected PrivilegeAllowPorts map[int64]struct{} @@ -119,6 +121,16 @@ func loadCommonConf(confFile string) error { DashboardPort = 0 } + tmpStr, ok = conf.Get("common", "dashboard_username") + if ok { + DashboardUsername = tmpStr + } + + tmpStr, ok = conf.Get("common", "dashboard_password") + if ok { + DashboardPassword = tmpStr + } + tmpStr, ok = conf.Get("common", "assets_dir") if ok { AssetsDir = tmpStr diff --git a/src/models/server/dashboard.go b/src/models/server/dashboard.go index 4be7a6de..8cb201b8 100644 --- a/src/models/server/dashboard.go +++ b/src/models/server/dashboard.go @@ -15,9 +15,11 @@ package server import ( + "encoding/base64" "fmt" "net" "net/http" + "strings" "time" "github.com/fatedier/frp/src/assets" @@ -38,7 +40,7 @@ func RunDashboardServer(addr string, port int64) (err error) { // view, see dashboard_view.go mux.Handle("/favicon.ico", http.FileServer(assets.FileSystem)) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(assets.FileSystem))) - mux.HandleFunc("/", viewDashboard) + mux.HandleFunc("/", use(viewDashboard, basicAuth)) address := fmt.Sprintf("%s:%d", addr, port) server := &http.Server{ @@ -58,3 +60,43 @@ func RunDashboardServer(addr string, port int64) (err error) { go server.Serve(ln) return } + +func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc { + for _, m := range middleware { + h = m(h) + } + + return h +} + +func basicAuth(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + + s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + if len(s) != 2 { + http.Error(w, "Not authorized", 401) + return + } + + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + http.Error(w, err.Error(), 401) + return + } + + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + http.Error(w, "Not authorized", 401) + return + } + + if pair[0] != DashboardUsername || pair[1] != DashboardPassword { + http.Error(w, "Not authorized", 401) + return + } + + h.ServeHTTP(w, r) + } +} diff --git a/src/utils/conn/conn.go b/src/utils/conn/conn.go index 9922758c..2434a089 100644 --- a/src/utils/conn/conn.go +++ b/src/utils/conn/conn.go @@ -16,9 +16,12 @@ package conn import ( "bufio" + "encoding/base64" "fmt" "io" "net" + "net/http" + "net/url" "strings" "sync" "time" @@ -104,9 +107,9 @@ func NewConn(conn net.Conn) (c *Conn) { return c } -func ConnectServer(host string, port int64) (c *Conn, err error) { +func ConnectServer(addr string) (c *Conn, err error) { c = &Conn{} - servertAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port)) + servertAddr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return } @@ -120,6 +123,49 @@ func ConnectServer(host string, port int64) (c *Conn, err error) { return c, nil } +func ConnectServerByHttpProxy(httpProxy string, serverAddr string) (c *Conn, err error) { + var proxyUrl *url.URL + if proxyUrl, err = url.Parse(httpProxy); err != nil { + return + } + + var proxyAuth string + if proxyUrl.User != nil { + proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(proxyUrl.User.String())) + } + + if proxyUrl.Scheme != "http" { + err = fmt.Errorf("Proxy URL scheme must be http, not [%s]", proxyUrl.Scheme) + return + } + + if c, err = ConnectServer(proxyUrl.Host); err != nil { + return + } + + req, err := http.NewRequest("CONNECT", "http://"+serverAddr, nil) + if err != nil { + return + } + if proxyAuth != "" { + req.Header.Set("Proxy-Authorization", proxyAuth) + } + req.Header.Set("User-Agent", "Mozilla/5.0") + req.Write(c.TcpConn) + + resp, err := http.ReadResponse(bufio.NewReader(c), req) + if err != nil { + return + } + resp.Body.Close() + if resp.StatusCode != 200 { + err = fmt.Errorf("ConnectServer using proxy error, StatusCode [%d]", resp.StatusCode) + return + } + + return +} + // if the tcpConn is different with c.TcpConn // you should call c.Close() first func (c *Conn) SetTcpConn(tcpConn net.Conn) { diff --git a/src/utils/version/version.go b/src/utils/version/version.go index b4532e05..b23ce428 100644 --- a/src/utils/version/version.go +++ b/src/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.8.1" +var version string = "0.9.0" func Full() string { return version diff --git a/src/utils/vhost/vhost.go b/src/utils/vhost/vhost.go index 93279b3b..12c8164c 100644 --- a/src/utils/vhost/vhost.go +++ b/src/utils/vhost/vhost.go @@ -71,6 +71,18 @@ func (v *VhostMuxer) Listen(name string, rewriteHost string) (l *Listener, err e func (v *VhostMuxer) getListener(name string) (l *Listener, exist bool) { v.mutex.RLock() defer v.mutex.RUnlock() + // first we check the full hostname + // if not exist, then check the wildcard_domain such as *.example.com + l, exist = v.registryMap[name] + if exist { + return l, exist + } + domainSplit := strings.Split(name, ".") + if len(domainSplit) < 3 { + return l, false + } + domainSplit[0] = "*" + name = strings.Join(domainSplit, ".") l, exist = v.registryMap[name] return l, exist } @@ -93,21 +105,26 @@ func (v *VhostMuxer) run() { func (v *VhostMuxer) handle(c *conn.Conn) { if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil { + c.Close() return } sConn, name, err := v.vhostFunc(c) if err != nil { + c.Close() return } name = strings.ToLower(name) + // get listener by hostname l, ok := v.getListener(name) if !ok { + c.Close() return } if err = sConn.SetDeadline(time.Time{}); err != nil { + c.Close() return } c.SetTcpConn(sConn) diff --git a/test/func_test.go b/test/func_test.go index 901ae7d2..3683c516 100644 --- a/test/func_test.go +++ b/test/func_test.go @@ -19,7 +19,7 @@ var ( ) func TestEchoServer(t *testing.T) { - c, err := conn.ConnectServer("0.0.0.0", ECHO_PORT) + c, err := conn.ConnectServer(fmt.Sprintf("0.0.0.0:%d", ECHO_PORT)) if err != nil { t.Fatalf("connect to echo server error: %v", err) }