diff --git a/src/assets/static/index.html b/src/assets/static/index.html index 6ac0650d..dc7f3626 100644 --- a/src/assets/static/index.html +++ b/src/assets/static/index.html @@ -63,7 +63,8 @@ listen_port: "<<< .ListenPort >>>", current_conns: <<< .CurrentConns >>> , domains: [ <<< range.CustomDomains >>> "<<< . >>>", <<< end >>> ], - stat: "<<< .Status >>>", + locations: [ <<< range.Locations >>> "<<< . >>>", <<< end >>> ], + stat: "<<< .StatusDesc >>>", use_encryption: "<<< .UseEncryption >>>", use_gzip: "<<< .UseGzip >>>", privilege_mode: "<<< .PrivilegeMode >>>", @@ -222,6 +223,10 @@ newrow += "Domains" + alldata[index].domains[domainindex] + ""; } + for (var locindex in alldata[index].locations) { + newrow += "Locations" + + alldata[index].locations[locindex] + ""; + } newrow += "Ip" + alldata[index].bind_addr + ""; newrow += "Status" + alldata[index].stat + ""; newrow += "Encryption" + alldata[index].use_encryption + ""; diff --git a/src/cmd/frps/control.go b/src/cmd/frps/control.go index db5c4087..dba77592 100644 --- a/src/cmd/frps/control.go +++ b/src/cmd/frps/control.go @@ -313,7 +313,8 @@ func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) { } // update metric's proxy status - metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip, s.PrivilegeMode, s.CustomDomains, s.ListenPort) + //metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip, s.PrivilegeMode, s.CustomDomains, s.ListenPort) + metric.SetProxyInfo(*s.ProxyServerConf) // start proxy and listen for user connections, no block err := s.Start(c) diff --git a/src/models/config/config.go b/src/models/config/config.go index f71d24af..366c0878 100644 --- a/src/models/config/config.go +++ b/src/models/config/config.go @@ -15,16 +15,26 @@ package config type BaseConf struct { - Name string - AuthToken string - Type string - UseEncryption bool - UseGzip bool - PrivilegeMode bool - PrivilegeToken string - PoolCount int64 - HostHeaderRewrite string - HttpUserName string - HttpPassWord string - SubDomain string + Name string `json:"name"` + AuthToken string `json:"-"` + Type string `json:"type"` + UseEncryption bool `json:"use_encryption"` + UseGzip bool `json:"use_gzip"` + PrivilegeMode bool `json:"privilege_mode"` + PrivilegeToken string `json:"-"` + PoolCount int64 `json:"pool_count"` + HostHeaderRewrite string `json:"host_header_rewrite"` + HttpUserName string `json:"http_username"` + HttpPassWord string `json:"-"` + SubDomain string `json:"subdomain"` +} + +type ProxyServerConf struct { + BaseConf + BindAddr string `json:"bind_addr"` + ListenPort int64 `json:"bind_port"` + CustomDomains []string `json:"custom_domains"` + Locations []string `json:"custom_locations"` + + Status int64 `json:"status"` } diff --git a/src/models/metric/server.go b/src/models/metric/server.go index fce7811a..d7775df9 100644 --- a/src/models/metric/server.go +++ b/src/models/metric/server.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "github.com/fatedier/frp/src/models/config" "github.com/fatedier/frp/src/models/consts" ) @@ -29,15 +30,8 @@ var ( ) type ServerMetric struct { - Name string `json:"name"` - Type string `json:"type"` - BindAddr string `json:"bind_addr"` - ListenPort int64 `json:"listen_port"` - CustomDomains []string `json:"custom_domains"` - Status string `json:"status"` - UseEncryption bool `json:"use_encryption"` - UseGzip bool `json:"use_gzip"` - PrivilegeMode bool `json:"privilege_mode"` + config.ProxyServerConf + StatusDesc string `json:"status_desc"` // statistics CurrentConns int64 `json:"current_conns"` @@ -110,24 +104,16 @@ func GetProxyMetrics(proxyName string) *ServerMetric { } } -func SetProxyInfo(proxyName string, proxyType, bindAddr string, - useEncryption, useGzip, privilegeMode bool, customDomains []string, - listenPort int64) { +func SetProxyInfo(p config.ProxyServerConf) { smMutex.Lock() - info, ok := ServerMetricInfoMap[proxyName] + info, ok := ServerMetricInfoMap[p.Name] if !ok { info = &ServerMetric{} info.Daily = make([]*DailyServerStats, 0) } - info.Name = proxyName - info.Type = proxyType - info.UseEncryption = useEncryption - info.UseGzip = useGzip - info.PrivilegeMode = privilegeMode - info.BindAddr = bindAddr - info.ListenPort = listenPort - info.CustomDomains = customDomains - ServerMetricInfoMap[proxyName] = info + + info.ProxyServerConf = p + ServerMetricInfoMap[p.Name] = info smMutex.Unlock() } @@ -137,7 +123,7 @@ func SetStatus(proxyName string, status int64) { smMutex.RUnlock() if ok { metric.mutex.Lock() - metric.Status = consts.StatusStr[status] + metric.StatusDesc = consts.StatusStr[status] metric.mutex.Unlock() } } diff --git a/src/models/server/config.go b/src/models/server/config.go index c246c6a8..aa963dad 100644 --- a/src/models/server/config.go +++ b/src/models/server/config.go @@ -296,6 +296,12 @@ func loadProxyConf(confFile string) (proxyServers map[string]*ProxyServer, err e } else { return proxyServers, fmt.Errorf("Parse conf error: proxy [%s] custom_domains must be set when type equals http", proxyServer.Name) } + + //location + locStr, loc_ok := section["custom_location"] + if loc_ok { + proxyServer.Locations = strings.Split(locStr, ",") + } } else if proxyServer.Type == "https" { // for https proxyServer.ListenPort = VhostHttpsPort @@ -318,9 +324,8 @@ func loadProxyConf(confFile string) (proxyServers map[string]*ProxyServer, err e } // set metric statistics of all proxies - for name, p := range proxyServers { - metric.SetProxyInfo(name, p.Type, p.BindAddr, p.UseEncryption, p.UseGzip, - p.PrivilegeMode, p.CustomDomains, p.ListenPort) + for _, p := range proxyServers { + metric.SetProxyInfo(*p.ProxyServerConf) } return proxyServers, nil } @@ -381,8 +386,7 @@ func CreateProxy(s *ProxyServer) error { } } ProxyServers[s.Name] = s - metric.SetProxyInfo(s.Name, s.Type, s.BindAddr, s.UseEncryption, s.UseGzip, - s.PrivilegeMode, s.CustomDomains, s.ListenPort) + metric.SetProxyInfo(*s.ProxyServerConf) s.Init() return nil } diff --git a/src/models/server/server.go b/src/models/server/server.go index 9fb8db16..30da60cd 100644 --- a/src/models/server/server.go +++ b/src/models/server/server.go @@ -33,13 +33,9 @@ type Listener interface { } type ProxyServer struct { - config.BaseConf - BindAddr string - ListenPort int64 - CustomDomains []string + *config.ProxyServerConf - Status int64 - CtlConn *conn.Conn // control connection with frpc + CtlConn *conn.Conn `json:"-"` // control connection with frpc 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 @@ -48,8 +44,9 @@ type ProxyServer struct { } func NewProxyServer() (p *ProxyServer) { + psc := &config.ProxyServerConf{CustomDomains: make([]string, 0)} p = &ProxyServer{ - CustomDomains: make([]string, 0), + ProxyServerConf: psc, } return p } @@ -101,6 +98,15 @@ func (p *ProxyServer) Compare(p2 *ProxyServer) bool { return false } } + + if len(p.Locations) != len(p2.Locations) { + return false + } + for i, _ := range p.Locations { + if p.Locations[i] != p2.Locations[i] { + return false + } + } return true } @@ -123,27 +129,13 @@ func (p *ProxyServer) Start(c *conn.Conn) (err error) { } p.listeners = append(p.listeners, l) } else if p.Type == "http" { - for _, domain := range p.CustomDomains { - l, err := VhostHttpMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord) - if err != nil { - return err - } - p.listeners = append(p.listeners, l) - } - if p.SubDomain != "" { - l, err := VhostHttpMuxer.Listen(p.SubDomain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord) - if err != nil { - return err - } + ls := VhostHttpMuxer.Listen(p.ProxyServerConf) + for _, l := range ls { p.listeners = append(p.listeners, l) } - } else if p.Type == "https" { - for _, domain := range p.CustomDomains { - l, err := VhostHttpsMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord) - if err != nil { - return err - } + ls := VhostHttpsMuxer.Listen(p.ProxyServerConf) + for _, l := range ls { p.listeners = append(p.listeners, l) } } diff --git a/src/utils/vhost/http.go b/src/utils/vhost/http.go index 623931a8..ad5926eb 100644 --- a/src/utils/vhost/http.go +++ b/src/utils/vhost/http.go @@ -45,6 +45,8 @@ func GetHttpRequestInfo(c *conn.Conn) (_ net.Conn, _ map[string]string, err erro // hostName tmpArr := strings.Split(request.Host, ":") reqInfoMap["Host"] = tmpArr[0] + reqInfoMap["Path"] = request.URL.Path + reqInfoMap["Scheme"] = request.URL.Scheme // Authorization authStr := request.Header.Get("Authorization") diff --git a/src/utils/vhost/https.go b/src/utils/vhost/https.go index 54a860a4..e21652f9 100644 --- a/src/utils/vhost/https.go +++ b/src/utils/vhost/https.go @@ -186,5 +186,6 @@ func GetHttpsHostname(c *conn.Conn) (sc net.Conn, _ map[string]string, err error return sc, reqInfoMap, err } reqInfoMap["Host"] = host + reqInfoMap["Scheme"] = "https" return sc, reqInfoMap, nil } diff --git a/src/utils/vhost/router.go b/src/utils/vhost/router.go new file mode 100644 index 00000000..d4735df1 --- /dev/null +++ b/src/utils/vhost/router.go @@ -0,0 +1,107 @@ +package vhost + +import ( + "sort" + "strings" + "sync" +) + +type VhostRouters struct { + RouterByDomain map[string][]*VhostRouter + mutex sync.RWMutex +} + +type VhostRouter struct { + name string + domain string + location string + listener *Listener +} + +func NewVhostRouters() *VhostRouters { + return &VhostRouters{ + RouterByDomain: make(map[string][]*VhostRouter), + } +} + +//sort by location +type ByLocation []*VhostRouter + +func (a ByLocation) Len() int { + return len(a) +} +func (a ByLocation) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} +func (a ByLocation) Less(i, j int) bool { + return strings.Compare(a[i].location, a[j].location) < 0 +} + +func (r *VhostRouters) add(name, domain string, locations []string, l *Listener) { + r.mutex.Lock() + defer r.mutex.Unlock() + + vrs, found := r.RouterByDomain[domain] + if !found { + vrs = make([]*VhostRouter, 0) + } + + for _, loc := range locations { + vr := &VhostRouter{ + name: name, + domain: domain, + location: loc, + listener: l, + } + vrs = append(vrs, vr) + } + + sort.Reverse(ByLocation(vrs)) + r.RouterByDomain[domain] = vrs +} + +func (r *VhostRouters) del(l *Listener) { + r.mutex.Lock() + defer r.mutex.Unlock() + + vrs, found := r.RouterByDomain[l.domain] + if !found { + return + } + + for i, vr := range vrs { + if vr.listener == l { + if len(vrs) > i+1 { + r.RouterByDomain[l.domain] = append(vrs[:i], vrs[i+1:]...) + } else { + r.RouterByDomain[l.domain] = vrs[:i] + } + } + } +} + +func (r *VhostRouters) get(rname string) (vr *VhostRouter, exist bool) { + r.mutex.RLock() + defer r.mutex.RUnlock() + + var domain, url string + tmparray := strings.SplitN(rname, ":", 2) + if len(tmparray) == 2 { + domain = tmparray[0] + url = tmparray[1] + } + + vrs, exist := r.RouterByDomain[domain] + if !exist { + return + } + + //can't support load balance,will to do + for _, vr = range vrs { + if strings.HasPrefix(url, vr.location) { + return vr, true + } + } + + return +} diff --git a/src/utils/vhost/vhost.go b/src/utils/vhost/vhost.go index c355bc71..fb0b5c8f 100644 --- a/src/utils/vhost/vhost.go +++ b/src/utils/vhost/vhost.go @@ -21,7 +21,9 @@ import ( "sync" "time" + "github.com/fatedier/frp/src/models/config" "github.com/fatedier/frp/src/utils/conn" + "github.com/fatedier/frp/src/utils/log" ) type muxFunc func(*conn.Conn) (net.Conn, map[string]string, error) @@ -29,71 +31,77 @@ type httpAuthFunc func(*conn.Conn, string, string, string) (bool, error) type hostRewriteFunc func(*conn.Conn, string) (net.Conn, error) type VhostMuxer struct { - listener *conn.Listener - timeout time.Duration - vhostFunc muxFunc - authFunc httpAuthFunc - rewriteFunc hostRewriteFunc - registryMap map[string]*Listener - mutex sync.RWMutex + listener *conn.Listener + timeout time.Duration + vhostFunc muxFunc + authFunc httpAuthFunc + rewriteFunc hostRewriteFunc + registryRouter *VhostRouters + mutex sync.RWMutex } func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { mux = &VhostMuxer{ - listener: listener, - timeout: timeout, - vhostFunc: vhostFunc, - authFunc: authFunc, - rewriteFunc: rewriteFunc, - registryMap: make(map[string]*Listener), + listener: listener, + timeout: timeout, + vhostFunc: vhostFunc, + authFunc: authFunc, + rewriteFunc: rewriteFunc, + registryRouter: NewVhostRouters(), } go mux.run() return mux, nil } // listen for a new domain name, if rewriteHost is not empty and rewriteFunc is not nil, then rewrite the host header to rewriteHost -func (v *VhostMuxer) Listen(name string, rewriteHost, userName, passWord string) (l *Listener, err error) { +func (v *VhostMuxer) Listen(p *config.ProxyServerConf) (ls []*Listener) { v.mutex.Lock() defer v.mutex.Unlock() - if _, exist := v.registryMap[name]; exist { - return nil, fmt.Errorf("domain name %s is already bound", name) - } - l = &Listener{ - name: name, - rewriteHost: rewriteHost, - userName: userName, - passWord: passWord, - mux: v, - accept: make(chan *conn.Conn), + ls = make([]*Listener, 0) + for _, domain := range p.CustomDomains { + l := &Listener{ + name: p.Name, + domain: domain, + locations: p.Locations, + rewriteHost: p.HostHeaderRewrite, + userName: p.HttpUserName, + passWord: p.HttpPassWord, + mux: v, + accept: make(chan *conn.Conn), + } + v.registryRouter.add(p.Name, domain, p.Locations, l) + ls = append(ls, l) } - v.registryMap[name] = l - return l, nil + return ls } 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 + + // // first we check the full hostname + vr, found := v.registryRouter.get(name) + if found { + return vr.listener, true } + + //if not exist, then check the wildcard_domain such as *.example.com domainSplit := strings.Split(name, ".") if len(domainSplit) < 3 { + log.Warn("can't found the router for %s", name) return l, false } domainSplit[0] = "*" name = strings.Join(domainSplit, ".") - 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) + vr, found = v.registryRouter.get(name) + if !found { + log.Warn("can't found the router for %s", name) + return + } + + return vr.listener, true } func (v *VhostMuxer) run() { @@ -120,6 +128,9 @@ func (v *VhostMuxer) handle(c *conn.Conn) { name := strings.ToLower(reqInfoMap["Host"]) // get listener by hostname + if reqInfoMap["Scheme"] == "http" { + name = name + ":" + reqInfoMap["Path"] + } l, ok := v.getListener(name) if !ok { c.Close() @@ -150,6 +161,8 @@ func (v *VhostMuxer) handle(c *conn.Conn) { type Listener struct { name string + domain string + locations []string rewriteHost string userName string passWord string @@ -177,7 +190,7 @@ func (l *Listener) Accept() (*conn.Conn, error) { } func (l *Listener) Close() error { - l.mux.unRegister(l.name) + l.mux.registryRouter.del(l) close(l.accept) return nil }