diff --git a/conf/frps.ini b/conf/frps.ini index 6163bc98..45ec8586 100644 --- a/conf/frps.ini +++ b/conf/frps.ini @@ -41,6 +41,11 @@ auth_token = 123 # if proxy type equals http, custom_domains must be set separated by commas custom_domains = web01.yourdomain.com,web01.yourdomain2.com +# http username and password are safety certification for http protoc +# if not set, you can access this custom_domains without certification +http_username = admin +http_password = admin + [web02] # if type equals https, vhost_https_port must be set type = https diff --git a/src/cmd/frpc/control.go b/src/cmd/frpc/control.go index d39a45d4..dcb8b0d7 100644 --- a/src/cmd/frpc/control.go +++ b/src/cmd/frpc/control.go @@ -150,6 +150,8 @@ func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) { ProxyType: cli.Type, PoolCount: cli.PoolCount, HostHeaderRewrite: cli.HostHeaderRewrite, + HttpUserName: cli.HttpUserName, + HttpPassWord: cli.HttpPassWord, Timestamp: nowTime, } if cli.PrivilegeMode { diff --git a/src/cmd/frps/control.go b/src/cmd/frps/control.go index 15620ff6..81e449cd 100644 --- a/src/cmd/frps/control.go +++ b/src/cmd/frps/control.go @@ -287,6 +287,8 @@ func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) { s.UseEncryption = req.UseEncryption s.UseGzip = req.UseGzip s.HostHeaderRewrite = req.HostHeaderRewrite + s.HttpUserName = req.HttpUserName + s.HttpPassWord = req.HttpPassWord if req.PoolCount > server.MaxPoolCount { s.PoolCount = server.MaxPoolCount } else if req.PoolCount < 0 { diff --git a/src/models/client/config.go b/src/models/client/config.go index ddc5d1d4..ca5ec5b7 100644 --- a/src/models/client/config.go +++ b/src/models/client/config.go @@ -156,6 +156,16 @@ func LoadConf(confFile string) (err error) { if ok { proxyClient.HostHeaderRewrite = tmpStr } + //http_username + tmpStr, ok = section["http_username"] + if ok { + proxyClient.HttpUserName = tmpStr + } + //http_password + tmpStr, ok = section["http_password"] + if ok { + proxyClient.HttpPassWord = tmpStr + } } // privilege_mode diff --git a/src/models/config/config.go b/src/models/config/config.go index 325dcb9b..9cf0071e 100644 --- a/src/models/config/config.go +++ b/src/models/config/config.go @@ -24,4 +24,6 @@ type BaseConf struct { PrivilegeToken string PoolCount int64 HostHeaderRewrite string + HttpUserName string + HttpPassWord string } diff --git a/src/models/msg/msg.go b/src/models/msg/msg.go index 253511af..f065df13 100644 --- a/src/models/msg/msg.go +++ b/src/models/msg/msg.go @@ -35,6 +35,8 @@ type ControlReq struct { RemotePort int64 `json:"remote_port"` CustomDomains []string `json:"custom_domains, omitempty"` HostHeaderRewrite string `json:"host_header_rewrite"` + HttpUserName string `json:"http_username"` + HttpPassWord string `json:"http_password"` Timestamp int64 `json:"timestamp"` } diff --git a/src/models/server/server.go b/src/models/server/server.go index d1502116..86019ea7 100644 --- a/src/models/server/server.go +++ b/src/models/server/server.go @@ -72,6 +72,8 @@ func NewProxyServerFromCtlMsg(req *msg.ControlReq) (p *ProxyServer) { } p.CustomDomains = req.CustomDomains p.HostHeaderRewrite = req.HostHeaderRewrite + p.HttpUserName = req.HttpUserName + p.HttpPassWord = req.HttpPassWord return } @@ -88,7 +90,8 @@ func (p *ProxyServer) Init() { func (p *ProxyServer) Compare(p2 *ProxyServer) bool { if p.Name != p2.Name || p.AuthToken != p2.AuthToken || p.Type != p2.Type || - p.BindAddr != p2.BindAddr || p.ListenPort != p2.ListenPort || p.HostHeaderRewrite != p2.HostHeaderRewrite { + p.BindAddr != p2.BindAddr || p.ListenPort != p2.ListenPort || p.HostHeaderRewrite != p2.HostHeaderRewrite || + p.HttpUserName != p2.HttpUserName || p.HttpPassWord != p2.HttpPassWord { return false } if len(p.CustomDomains) != len(p2.CustomDomains) { @@ -122,7 +125,7 @@ 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) + l, err := VhostHttpMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord) if err != nil { return err } @@ -130,7 +133,7 @@ func (p *ProxyServer) Start(c *conn.Conn) (err error) { } } else if p.Type == "https" { for _, domain := range p.CustomDomains { - l, err := VhostHttpsMuxer.Listen(domain, p.HostHeaderRewrite) + l, err := VhostHttpsMuxer.Listen(domain, p.HostHeaderRewrite, p.HttpUserName, p.HttpPassWord) if err != nil { return err } diff --git a/src/utils/vhost/http.go b/src/utils/vhost/http.go index 0d1d8e07..623931a8 100644 --- a/src/utils/vhost/http.go +++ b/src/utils/vhost/http.go @@ -17,6 +17,7 @@ package vhost import ( "bufio" "bytes" + "encoding/base64" "fmt" "io" "net" @@ -33,21 +34,29 @@ type HttpMuxer struct { *VhostMuxer } -func GetHttpHostname(c *conn.Conn) (_ net.Conn, routerName string, err error) { +func GetHttpRequestInfo(c *conn.Conn) (_ net.Conn, _ map[string]string, err error) { + reqInfoMap := make(map[string]string, 0) sc, rd := newShareConn(c.TcpConn) request, err := http.ReadRequest(bufio.NewReader(rd)) if err != nil { - return sc, "", err + return sc, reqInfoMap, err } + // hostName tmpArr := strings.Split(request.Host, ":") - routerName = tmpArr[0] + reqInfoMap["Host"] = tmpArr[0] + + // Authorization + authStr := request.Header.Get("Authorization") + if authStr != "" { + reqInfoMap["Authorization"] = authStr + } request.Body.Close() - return sc, routerName, nil + return sc, reqInfoMap, nil } func NewHttpMuxer(listener *conn.Listener, timeout time.Duration) (*HttpMuxer, error) { - mux, err := NewVhostMuxer(listener, GetHttpHostname, HttpHostNameRewrite, timeout) + mux, err := NewVhostMuxer(listener, GetHttpRequestInfo, HttpAuthFunc, HttpHostNameRewrite, timeout) return &HttpMuxer{mux}, err } @@ -169,3 +178,38 @@ func changeHostName(buff *bytes.Buffer, rewriteHost string) (_ []byte, err error retBuf.Write(peek) return retBuf.Bytes(), err } + +func HttpAuthFunc(c *conn.Conn, userName, passWord, authorization string) (bAccess bool, err error) { + s := strings.SplitN(authorization, " ", 2) + if len(s) != 2 { + res := noAuthResponse() + res.Write(c.TcpConn) + return + } + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + return + } + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + return + } + if pair[0] != userName || pair[1] != passWord { + return + } + return true, nil +} + +func noAuthResponse() *http.Response { + header := make(map[string][]string) + header["WWW-Authenticate"] = []string{`Basic realm="Restricted"`} + res := &http.Response{ + Status: "401 Not authorized", + StatusCode: 401, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: header, + } + return res +} diff --git a/src/utils/vhost/https.go b/src/utils/vhost/https.go index a82011b4..54a860a4 100644 --- a/src/utils/vhost/https.go +++ b/src/utils/vhost/https.go @@ -48,7 +48,7 @@ type HttpsMuxer struct { } func NewHttpsMuxer(listener *conn.Listener, timeout time.Duration) (*HttpsMuxer, error) { - mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, timeout) + mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, nil, timeout) return &HttpsMuxer{mux}, err } @@ -178,11 +178,13 @@ func readHandshake(rd io.Reader) (host string, err error) { return } -func GetHttpsHostname(c *conn.Conn) (sc net.Conn, routerName string, err error) { +func GetHttpsHostname(c *conn.Conn) (sc net.Conn, _ map[string]string, err error) { + reqInfoMap := make(map[string]string, 0) sc, rd := newShareConn(c.TcpConn) host, err := readHandshake(rd) if err != nil { - return sc, "", err + return sc, reqInfoMap, err } - return sc, host, nil + reqInfoMap["Host"] = host + return sc, reqInfoMap, nil } diff --git a/src/utils/vhost/vhost.go b/src/utils/vhost/vhost.go index 12c8164c..fa33b324 100644 --- a/src/utils/vhost/vhost.go +++ b/src/utils/vhost/vhost.go @@ -1,5 +1,3 @@ -// 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 @@ -26,23 +24,26 @@ import ( "github.com/fatedier/frp/src/utils/conn" ) -type muxFunc func(*conn.Conn) (net.Conn, string, error) +type muxFunc func(*conn.Conn) (net.Conn, map[string]string, error) +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 } -func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { +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), } @@ -51,7 +52,7 @@ func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, rewriteFunc hostR } // 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 string) (l *Listener, err error) { +func (v *VhostMuxer) Listen(name string, rewriteHost, userName, passWord string) (l *Listener, err error) { v.mutex.Lock() defer v.mutex.Unlock() if _, exist := v.registryMap[name]; exist { @@ -61,6 +62,8 @@ func (v *VhostMuxer) Listen(name string, rewriteHost string) (l *Listener, err e l = &Listener{ name: name, rewriteHost: rewriteHost, + userName: userName, + passWord: passWord, mux: v, accept: make(chan *conn.Conn), } @@ -109,13 +112,13 @@ func (v *VhostMuxer) handle(c *conn.Conn) { return } - sConn, name, err := v.vhostFunc(c) + sConn, reqInfoMap, err := v.vhostFunc(c) if err != nil { c.Close() return } - name = strings.ToLower(name) + name := strings.ToLower(reqInfoMap["Host"]) // get listener by hostname l, ok := v.getListener(name) if !ok { @@ -123,6 +126,20 @@ func (v *VhostMuxer) handle(c *conn.Conn) { return } + // if authFunc is exist and userName/password is set + // verify user access + fmt.Printf("reqInfo: %+v\n", reqInfoMap) + if l.mux.authFunc != nil && + l.userName != "" && l.passWord != "" { + bAccess, err := l.mux.authFunc(c, l.userName, l.passWord, reqInfoMap["Authorization"]) + if bAccess == false || err != nil { + res := noAuthResponse() + res.Write(c.TcpConn) + c.Close() + return + } + } + if err = sConn.SetDeadline(time.Time{}); err != nil { c.Close() return @@ -135,6 +152,8 @@ func (v *VhostMuxer) handle(c *conn.Conn) { type Listener struct { name string rewriteHost string + userName string + passWord string mux *VhostMuxer // for closing VhostMuxer accept chan *conn.Conn } @@ -154,6 +173,7 @@ func (l *Listener) Accept() (*conn.Conn, error) { } conn.SetTcpConn(sConn) } + return conn, nil }