From f650d3f330d49e36df2dc5409b0559b6e7b8a647 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 18 Apr 2016 15:16:40 +0800 Subject: [PATCH] all: support for virtual host --- conf/frpc.ini | 18 ++- conf/frps.ini | 18 ++- src/frp/cmd/frps/control.go | 2 +- src/frp/cmd/frps/main.go | 18 ++- src/frp/models/client/client.go | 1 + src/frp/models/client/config.go | 10 ++ src/frp/models/server/config.go | 63 ++++++++-- src/frp/models/server/server.go | 118 +++++++++++-------- src/frp/utils/conn/conn.go | 32 +++-- src/frp/utils/version/version.go | 2 +- src/frp/utils/vhost/vhost.go | 193 +++++++++++++++++++++++++++++++ 11 files changed, 400 insertions(+), 75 deletions(-) create mode 100644 src/frp/utils/vhost/vhost.go diff --git a/conf/frpc.ini b/conf/frpc.ini index 86679e6d..28f4e6d2 100644 --- a/conf/frpc.ini +++ b/conf/frpc.ini @@ -9,9 +9,23 @@ log_level = info # for authentication auth_token = 123 -# test1 is the proxy name same as server's configuration -[test1] +# ssh is the proxy name same as server's configuration +[ssh] +# tcp | http, default is tcp +type = tcp local_ip = 127.0.0.1 local_port = 22 # true or false, if true, messages between frps and frpc will be encrypted, default is false use_encryption = true + +# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02, the domains are set in frps.ini +[web01] +type = http +local_ip = 127.0.0.1 +local_port = 80 +use_encryption = true + +[web02] +type = http +local_ip = 127.0.0.1 +local_port = 8000 diff --git a/conf/frps.ini b/conf/frps.ini index cfb9e7b7..79bbbdfa 100644 --- a/conf/frps.ini +++ b/conf/frps.ini @@ -2,13 +2,27 @@ [common] bind_addr = 0.0.0.0 bind_port = 7000 +# optional +vhost_http_port = 80 # console or real logFile path like ./frps.log log_file = ./frps.log # debug, info, warn, error log_level = info -# test1 is the proxy name, client will use this name and auth_token to connect to server -[test1] +# ssh is the proxy name, client will use this name and auth_token to connect to server +[ssh] +type = tcp auth_token = 123 bind_addr = 0.0.0.0 listen_port = 6000 + +[web01] +type = http +auth_token = 123 +# if proxy type equals http, custom_domains must be set separated by commas +custom_domains = web01.yourdomain.com,web01.yourdomain2.com + +[web02] +type = http +auth_token = 123 +custom_domains = web02.yourdomain.com diff --git a/src/frp/cmd/frps/control.go b/src/frp/cmd/frps/control.go index 6566cd73..c8c78271 100644 --- a/src/frp/cmd/frps/control.go +++ b/src/frp/cmd/frps/control.go @@ -30,7 +30,7 @@ import ( func ProcessControlConn(l *conn.Listener) { for { - c, err := l.GetConn() + c, err := l.Accept() if err != nil { return } diff --git a/src/frp/cmd/frps/main.go b/src/frp/cmd/frps/main.go index 4c918f06..bd68f0c0 100644 --- a/src/frp/cmd/frps/main.go +++ b/src/frp/cmd/frps/main.go @@ -19,6 +19,7 @@ import ( "os" "strconv" "strings" + "time" docopt "github.com/docopt/docopt-go" @@ -26,6 +27,7 @@ import ( "frp/utils/conn" "frp/utils/log" "frp/utils/version" + "frp/utils/vhost" ) var ( @@ -92,8 +94,20 @@ func main() { l, err := conn.Listen(server.BindAddr, server.BindPort) if err != nil { - log.Error("Create listener error, %v", err) - os.Exit(-1) + log.Error("Create server listener error, %v", err) + os.Exit(1) + } + + if server.VhostHttpPort != 0 { + vhostListener, err := conn.Listen(server.BindAddr, server.VhostHttpPort) + if err != nil { + log.Error("Create vhost http listener error, %v", err) + os.Exit(1) + } + server.VhostMuxer, err = vhost.NewHttpMuxer(vhostListener, 30*time.Second) + if err != nil { + log.Error("Create vhost httpMuxer error, %v", err) + } } log.Info("Start frps success") diff --git a/src/frp/models/client/client.go b/src/frp/models/client/client.go index b0b947b8..6165eee1 100644 --- a/src/frp/models/client/client.go +++ b/src/frp/models/client/client.go @@ -31,6 +31,7 @@ type ProxyClient struct { AuthToken string LocalIp string LocalPort int64 + Type string UseEncryption bool } diff --git a/src/frp/models/client/config.go b/src/frp/models/client/config.go index 3c09c751..466ff40d 100644 --- a/src/frp/models/client/config.go +++ b/src/frp/models/client/config.go @@ -105,6 +105,16 @@ func LoadConf(confFile string) (err error) { return fmt.Errorf("Parse ini file error: proxy [%s] local_port not found", proxyClient.Name) } + // type + proxyClient.Type = "tcp" + typeStr, ok := section["type"] + if ok { + if typeStr != "tcp" && typeStr != "http" { + return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyClient.Name) + } + proxyClient.Type = typeStr + } + // use_encryption proxyClient.UseEncryption = false useEncryptionStr, ok := section["use_encryption"] diff --git a/src/frp/models/server/config.go b/src/frp/models/server/config.go index 23fd9699..8f04c21a 100644 --- a/src/frp/models/server/config.go +++ b/src/frp/models/server/config.go @@ -17,19 +17,25 @@ package server import ( "fmt" "strconv" + "strings" ini "github.com/vaughan0/go-ini" + + "frp/utils/vhost" ) // common config var ( BindAddr string = "0.0.0.0" BindPort int64 = 7000 + VhostHttpPort int64 = 0 // if VhostHttpPort equals 0, do not listen a public port for http LogFile string = "console" LogWay string = "console" // console or file LogLevel string = "info" HeartBeatTimeout int64 = 90 UserConnTimeout int64 = 10 + + VhostMuxer *vhost.HttpMuxer ) var ProxyServers map[string]*ProxyServer = make(map[string]*ProxyServer) @@ -54,6 +60,13 @@ func LoadConf(confFile string) (err error) { BindPort, _ = strconv.ParseInt(tmpStr, 10, 64) } + tmpStr, ok = conf.Get("common", "vhost_http_port") + if ok { + VhostHttpPort, _ = strconv.ParseInt(tmpStr, 10, 64) + } else { + VhostHttpPort = 0 + } + tmpStr, ok = conf.Get("common", "log_file") if ok { LogFile = tmpStr @@ -73,26 +86,52 @@ func LoadConf(confFile string) (err error) { for name, section := range conf { if name != "common" { proxyServer := &ProxyServer{} + proxyServer.CustomDomains = make([]string, 0) proxyServer.Name = name + proxyServer.Type, ok = section["type"] + if ok { + if proxyServer.Type != "tcp" && proxyServer.Type != "http" { + return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyServer.Name) + } + } else { + proxyServer.Type = "tcp" + } + proxyServer.AuthToken, ok = section["auth_token"] if !ok { return fmt.Errorf("Parse ini file error: proxy [%s] no auth_token found", proxyServer.Name) } - proxyServer.BindAddr, ok = section["bind_addr"] - if !ok { - proxyServer.BindAddr = "0.0.0.0" - } - - portStr, ok := section["listen_port"] - if ok { - proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64) - if err != nil { - return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name) + // for tcp + if proxyServer.Type == "tcp" { + proxyServer.BindAddr, ok = section["bind_addr"] + if !ok { + proxyServer.BindAddr = "0.0.0.0" + } + + portStr, ok := section["listen_port"] + if ok { + proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64) + if err != nil { + return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name) + } + } else { + return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name) + } + } else if proxyServer.Type == "http" { + // for http + domainStr, ok := section["custom_domains"] + if ok { + var suffix string + if VhostHttpPort != 80 { + suffix = fmt.Sprintf(":%d", VhostHttpPort) + } + proxyServer.CustomDomains = strings.Split(domainStr, ",") + for i, domain := range proxyServer.CustomDomains { + proxyServer.CustomDomains[i] = strings.ToLower(strings.TrimSpace(domain)) + suffix + } } - } else { - return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name) } proxyServer.Init() diff --git a/src/frp/models/server/server.go b/src/frp/models/server/server.go index d6e06daf..a6338dfe 100644 --- a/src/frp/models/server/server.go +++ b/src/frp/models/server/server.go @@ -24,15 +24,22 @@ import ( "frp/utils/log" ) +type Listener interface { + Accept() (*conn.Conn, error) + Close() error +} + type ProxyServer struct { Name string AuthToken string - UseEncryption bool + Type string BindAddr string ListenPort int64 - Status int64 + UseEncryption bool + CustomDomains []string - listener *conn.Listener // accept new connection from remote users + Status int64 + listeners []Listener // accept new connection from remote users ctlMsgChan chan int64 // every time accept a new user conn, put "1" to the channel workConnChan chan *conn.Conn // get new work conns from control goroutine userConnList *list.List // store user conns @@ -44,6 +51,7 @@ func (p *ProxyServer) Init() { p.workConnChan = make(chan *conn.Conn) p.ctlMsgChan = make(chan int64) p.userConnList = list.New() + p.listeners = make([]Listener, 0) } func (p *ProxyServer) Lock() { @@ -57,57 +65,71 @@ func (p *ProxyServer) Unlock() { // start listening for user conns func (p *ProxyServer) Start() (err error) { p.Init() - p.listener, err = conn.Listen(p.BindAddr, p.ListenPort) - if err != nil { - return err + if p.Type == "tcp" { + l, err := conn.Listen(p.BindAddr, p.ListenPort) + if err != nil { + return err + } + p.listeners = append(p.listeners, l) + } else if p.Type == "http" { + for _, domain := range p.CustomDomains { + l, err := VhostMuxer.Listen(domain) + if err != nil { + return err + } + p.listeners = append(p.listeners, l) + } } p.Status = consts.Working // start a goroutine for listener to accept user connection - go func() { - for { - // block - // if listener is closed, err returned - c, err := p.listener.GetConn() - if err != nil { - log.Info("ProxyName [%s], listener is closed", p.Name) - return - } - log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr()) - - // insert into list - p.Lock() - if p.Status != consts.Working { - log.Debug("ProxyName [%s] is not working, new user conn close", p.Name) - c.Close() - p.Unlock() - return - } - p.userConnList.PushBack(c) - p.Unlock() - - // put msg to control conn - p.ctlMsgChan <- 1 - - // set timeout - time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() { - p.Lock() - defer p.Unlock() - element := p.userConnList.Front() - if element == nil { + for _, listener := range p.listeners { + go func(l Listener) { + for { + // block + // if listener is closed, err returned + c, err := l.Accept() + if err != nil { + log.Info("ProxyName [%s], listener is closed", p.Name) return } + log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr()) - userConn := element.Value.(*conn.Conn) - if userConn == c { - log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr()) + // insert into list + p.Lock() + if p.Status != consts.Working { + log.Debug("ProxyName [%s] is not working, new user conn close", p.Name) + c.Close() + p.Unlock() + return } - }) - } - }() + p.userConnList.PushBack(c) + p.Unlock() - // start another goroutine for join two conns from client and user + // put msg to control conn + p.ctlMsgChan <- 1 + + // set timeout + time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() { + p.Lock() + defer p.Unlock() + element := p.userConnList.Front() + if element == nil { + return + } + + userConn := element.Value.(*conn.Conn) + if userConn == c { + log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr()) + userConn.Close() + } + }) + } + }(listener) + } + + // start another goroutine for join two conns from frpc and user go func() { for { workConn, ok := <-p.workConnChan @@ -149,8 +171,12 @@ func (p *ProxyServer) Close() { p.Lock() if p.Status != consts.Closed { p.Status = consts.Closed - if p.listener != nil { - p.listener.Close() + if len(p.listeners) != 0 { + for _, l := range p.listeners { + if l != nil { + l.Close() + } + } } close(p.ctlMsgChan) close(p.workConnChan) diff --git a/src/frp/utils/conn/conn.go b/src/frp/utils/conn/conn.go index eb064c4a..29b4d22f 100644 --- a/src/frp/utils/conn/conn.go +++ b/src/frp/utils/conn/conn.go @@ -20,6 +20,7 @@ import ( "io" "net" "sync" + "time" "frp/utils/log" "frp/utils/pcrypto" @@ -28,7 +29,7 @@ import ( type Listener struct { addr net.Addr l *net.TCPListener - conns chan *Conn + accept chan *Conn closeFlag bool } @@ -42,7 +43,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { l = &Listener{ addr: listener.Addr(), l: listener, - conns: make(chan *Conn), + accept: make(chan *Conn), closeFlag: false, } @@ -61,7 +62,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { closeFlag: false, } c.Reader = bufio.NewReader(c.TcpConn) - l.conns <- c + l.accept <- c } }() return l, err @@ -69,30 +70,38 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { // wait util get one new connection or listener is closed // if listener is closed, err returned -func (l *Listener) GetConn() (conn *Conn, err error) { - var ok bool - conn, ok = <-l.conns +func (l *Listener) Accept() (*Conn, error) { + conn, ok := <-l.accept if !ok { return conn, fmt.Errorf("channel close") } return conn, nil } -func (l *Listener) Close() { +func (l *Listener) Close() error { if l.l != nil && l.closeFlag == false { l.closeFlag = true l.l.Close() - close(l.conns) + close(l.accept) } + return nil } // wrap for TCPConn type Conn struct { - TcpConn *net.TCPConn + TcpConn net.Conn Reader *bufio.Reader closeFlag bool } +func NewConn(conn net.Conn) (c *Conn) { + c = &Conn{} + c.TcpConn = conn + c.Reader = bufio.NewReader(c.TcpConn) + c.closeFlag = false + return c +} + func ConnectServer(host string, port int64) (c *Conn, err error) { c = &Conn{} servertAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", host, port)) @@ -131,6 +140,11 @@ func (c *Conn) Write(content string) (err error) { } +func (c *Conn) SetDeadline(t time.Time) error { + err := c.TcpConn.SetDeadline(t) + return err +} + func (c *Conn) Close() { if c.TcpConn != nil && c.closeFlag == false { c.closeFlag = true diff --git a/src/frp/utils/version/version.go b/src/frp/utils/version/version.go index 075a4c8d..7eb5272f 100644 --- a/src/frp/utils/version/version.go +++ b/src/frp/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.3.0" +var version string = "0.5.0" func Full() string { return version diff --git a/src/frp/utils/vhost/vhost.go b/src/frp/utils/vhost/vhost.go new file mode 100644 index 00000000..d832f537 --- /dev/null +++ b/src/frp/utils/vhost/vhost.go @@ -0,0 +1,193 @@ +// Copyright 2016 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vhost + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "frp/utils/conn" +) + +type muxFunc func(*conn.Conn) (net.Conn, string, error) + +type VhostMuxer struct { + listener *conn.Listener + timeout time.Duration + vhostFunc muxFunc + registryMap map[string]*Listener + mutex sync.RWMutex +} + +func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, timeout time.Duration) (mux *VhostMuxer, err error) { + mux = &VhostMuxer{ + listener: listener, + timeout: timeout, + vhostFunc: vhostFunc, + registryMap: make(map[string]*Listener), + } + go mux.run() + return mux, nil +} + +func (v *VhostMuxer) Listen(name string) (l *Listener, err error) { + v.mutex.Lock() + defer v.mutex.Unlock() + if _, exist := v.registryMap[name]; exist { + return nil, fmt.Errorf("name %s is already bound", name) + } + + l = &Listener{ + name: name, + mux: v, + accept: make(chan *conn.Conn), + } + v.registryMap[name] = l + return l, nil +} + +func (v *VhostMuxer) getListener(name string) (l *Listener, exist bool) { + v.mutex.RLock() + defer v.mutex.RUnlock() + l, exist = v.registryMap[name] + return l, exist +} + +func (v *VhostMuxer) unRegister(name string) { + v.mutex.Lock() + defer v.mutex.Unlock() + delete(v.registryMap, name) +} + +func (v *VhostMuxer) run() { + for { + conn, err := v.listener.Accept() + if err != nil { + return + } + go v.handle(conn) + } +} + +func (v *VhostMuxer) handle(c *conn.Conn) { + if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil { + return + } + + sConn, name, err := v.vhostFunc(c) + if err != nil { + return + } + + name = strings.ToLower(name) + + l, ok := v.getListener(name) + if !ok { + return + } + + if err = sConn.SetDeadline(time.Time{}); err != nil { + return + } + c.TcpConn = sConn + + l.accept <- c +} + +type HttpMuxer struct { + *VhostMuxer +} + +func GetHttpHostname(c *conn.Conn) (_ net.Conn, routerName string, err error) { + sc, rd := newShareConn(c.TcpConn) + + request, err := http.ReadRequest(bufio.NewReader(rd)) + if err != nil { + return sc, "", err + } + routerName = request.Host + request.Body.Close() + + return sc, routerName, nil +} + +func NewHttpMuxer(listener *conn.Listener, timeout time.Duration) (*HttpMuxer, error) { + mux, err := NewVhostMuxer(listener, GetHttpHostname, timeout) + return &HttpMuxer{mux}, err +} + +type Listener struct { + name string + mux *VhostMuxer // for closing VhostMuxer + accept chan *conn.Conn +} + +func (l *Listener) Accept() (*conn.Conn, error) { + conn, ok := <-l.accept + if !ok { + return nil, fmt.Errorf("Listener closed") + } + return conn, nil +} + +func (l *Listener) Close() error { + l.mux.unRegister(l.name) + close(l.accept) + return nil +} + +func (l *Listener) Name() string { + return l.name +} + +type sharedConn struct { + net.Conn + sync.Mutex + buff *bytes.Buffer +} + +func newShareConn(conn net.Conn) (*sharedConn, io.Reader) { + sc := &sharedConn{ + Conn: conn, + buff: bytes.NewBuffer(make([]byte, 0, 1024)), + } + return sc, io.TeeReader(conn, sc.buff) +} + +func (sc *sharedConn) Read(p []byte) (n int, err error) { + sc.Lock() + if sc.buff == nil { + sc.Unlock() + return sc.Conn.Read(p) + } + n, err = sc.buff.Read(p) + + if err == io.EOF { + sc.buff = nil + var n2 int + n2, err = sc.Conn.Read(p[n:]) + + n += n2 + } + sc.Unlock() + return +}