From 24b71f987cbf344502e19fc203e67ff62ef11f7e Mon Sep 17 00:00:00 2001 From: Akkariin Date: Wed, 21 Aug 2019 15:37:33 +0800 Subject: [PATCH] Add api feature --- conf/frps_full.ini | 9 ++ extend/api/api.go | 186 +++++++++++++++++++++++++++++++++ models/config/server_common.go | 24 +++++ server/control.go | 23 ++++ server/service.go | 31 ++++++ 5 files changed, 273 insertions(+) mode change 100644 => 100755 conf/frps_full.ini create mode 100755 extend/api/api.go mode change 100644 => 100755 models/config/server_common.go mode change 100644 => 100755 server/control.go mode change 100644 => 100755 server/service.go diff --git a/conf/frps_full.ini b/conf/frps_full.ini old mode 100644 new mode 100755 index a8c0e635..e4c36aa6 --- a/conf/frps_full.ini +++ b/conf/frps_full.ini @@ -68,3 +68,12 @@ tcp_mux = true # custom 404 page for HTTP requests # custom_404_page = /path/to/404.html + +# enable or disable the frps API +api_enable = false + +# API base request url. +# api_baseurl = https://api.example.com/ + +# API token used to verify the server. +# api_token = 12345667890 diff --git a/extend/api/api.go b/extend/api/api.go new file mode 100755 index 00000000..4b3fe58d --- /dev/null +++ b/extend/api/api.go @@ -0,0 +1,186 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + + "github.com/fatedier/frp/models/msg" +) + +type Service struct { + Host url.URL +} + +func NewService(host string) (s *Service, err error) { + u, err := url.Parse(host) + if err != nil { + return + } + return &Service{*u}, nil +} + +// CheckToken 校验客户端 token +func (s Service) CheckToken(user string, token string, timestamp int64, stk string) (ok bool, err error) { + values := url.Values{} + values.Set("action", "checktoken") + values.Set("user", user) + values.Set("token", token) + values.Set("timestamp", fmt.Sprintf("%d", timestamp)) + values.Set("apitoken", stk) + s.Host.RawQuery = values.Encode() + defer func(u *url.URL) { + u.RawQuery = "" + }(&s.Host) + resp, err := http.Get(s.Host.String()) + if err != nil { + return false, err + } + if resp.StatusCode != http.StatusOK { + return false, ErrHTTPStatus{ + Status: resp.StatusCode, + Text: resp.Status, + } + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + response := ResponseCheckToken{} + if err = json.Unmarshal(body, &response); err != nil { + return false, err + } + if !response.Success { + return false, ErrCheckTokenFail{response.Message} + } + return true, nil +} + +// CheckProxy 校验客户端代理 +func (s Service) CheckProxy(user string, pMsg *msg.NewProxy, timestamp int64, stk string) (ok bool, err error) { + + domains, err := json.Marshal(pMsg.CustomDomains) + if err != nil { + return false, err + } + + headers, err := json.Marshal(pMsg.Headers) + if err != nil { + return false, err + } + + locations, err := json.Marshal(pMsg.Locations) + if err != nil { + return false, err + } + + values := url.Values{} + + // API Basic + values.Set("action", "checkproxy") + values.Set("user", user) + values.Set("timestamp", fmt.Sprintf("%d", timestamp)) + values.Set("apitoken", stk) + + // Proxies basic info + values.Set("proxy_name", pMsg.ProxyName) + values.Set("proxy_type", pMsg.ProxyType) + values.Set("use_encryption", BoolToString(pMsg.UseEncryption)) + values.Set("use_compression", BoolToString(pMsg.UseCompression)) + + // Http Proxies + values.Set("domain", string(domains)) + values.Set("subdomain", pMsg.SubDomain) + + // Headers + values.Set("locations", string(locations)) + values.Set("http_user", pMsg.HttpUser) + values.Set("http_pwd", pMsg.HttpPwd) + values.Set("host_header_rewrite", pMsg.HostHeaderRewrite) + values.Set("headers", string(headers)) + + // Tcp & Udp & Stcp + values.Set("remote_port", strconv.Itoa(pMsg.RemotePort)) + + // Stcp & Xtcp + values.Set("sk", pMsg.Sk) + + // Load balance + values.Set("group", pMsg.Group) + values.Set("group_key", pMsg.GroupKey) + + s.Host.RawQuery = values.Encode() + defer func(u *url.URL) { + u.RawQuery = "" + }(&s.Host) + resp, err := http.Get(s.Host.String()) + if err != nil { + return false, err + } + if resp.StatusCode != http.StatusOK { + return false, ErrHTTPStatus{ + Status: resp.StatusCode, + Text: resp.Status, + } + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + response := ResponseCheckProxy{} + if err = json.Unmarshal(body, &response); err != nil { + return false, err + } + if !response.Success { + return false, ErrCheckProxyFail{response.Message} + } + return true, nil +} + +func BoolToString(val bool) (str string) { + if val { + return "true" + } else { + return "false" + } +} + +type ErrHTTPStatus struct { + Status int `json:"status"` + Text string `json:"massage"` +} + +func (e ErrHTTPStatus) Error() string { + return fmt.Sprintf("%s", e.Text) +} + +type ResponseCheckToken struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +type ResponseCheckProxy struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +type ErrCheckTokenFail struct { + Message string +} + +type ErrCheckProxyFail struct { + Message string +} + +func (e ErrCheckTokenFail) Error() string { + return e.Message +} + +func (e ErrCheckProxyFail) Error() string { + return e.Message +} diff --git a/models/config/server_common.go b/models/config/server_common.go old mode 100644 new mode 100755 index 1e54bdd8..60a5e870 --- a/models/config/server_common.go +++ b/models/config/server_common.go @@ -76,6 +76,12 @@ type ServerCommonConf struct { MaxPortsPerClient int64 `json:"max_ports_per_client"` HeartBeatTimeout int64 `json:"heart_beat_timeout"` UserConnTimeout int64 `json:"user_conn_timeout"` + + // API + EnableApi bool `json:"api_enable"` + ApiBaseUrl string `json:"api_baseurl"` + ApiToken string `json:"api_token"` + } func GetDefaultServerConf() *ServerCommonConf { @@ -106,6 +112,9 @@ func GetDefaultServerConf() *ServerCommonConf { HeartBeatTimeout: 90, UserConnTimeout: 10, Custom404Page: "", + EnableApi: false, + ApiBaseUrl: "", + ApiToken: "", } } @@ -308,6 +317,21 @@ func UnmarshalServerConfFromIni(defaultCfg *ServerCommonConf, content string) (c cfg.HeartBeatTimeout = v } } + + if tmpStr, ok = conf.Get("common", "api_enable"); ok && tmpStr == "false" { + cfg.EnableApi = false + } else { + cfg.EnableApi = true + } + + if tmpStr, ok = conf.Get("common", "api_baseurl"); ok { + cfg.ApiBaseUrl = tmpStr + } + + if tmpStr, ok = conf.Get("common", "api_token"); ok { + cfg.ApiToken = tmpStr + } + return } diff --git a/server/control.go b/server/control.go old mode 100644 new mode 100755 index 8374ab20..c1ec52b7 --- a/server/control.go +++ b/server/control.go @@ -35,6 +35,8 @@ import ( "github.com/fatedier/golib/control/shutdown" "github.com/fatedier/golib/crypto" "github.com/fatedier/golib/errors" + + "github.com/fatedier/frp/extend/api" ) type ControlManager struct { @@ -416,6 +418,27 @@ func (ctl *Control) manager() { func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) { var pxyConf config.ProxyConf + + s, err := api.NewService(g.GlbServerCfg.ApiBaseUrl) + + if err != nil { + return remoteAddr, err + } + + if g.GlbServerCfg.EnableApi { + + nowTime := time.Now().Unix() + ok, err := s.CheckProxy(ctl.loginMsg.User, pxyMsg, nowTime, g.GlbServerCfg.ApiToken) + + if err != nil { + return remoteAddr, err + } + + if !ok { + return remoteAddr, fmt.Errorf("invalid proxy configuration") + } + } + // Load configures from NewProxy message and check. pxyConf, err = config.NewProxyConfFromMsg(pxyMsg) if err != nil { diff --git a/server/service.go b/server/service.go old mode 100644 new mode 100755 index e3a1d117..35c45fbd --- a/server/service.go +++ b/server/service.go @@ -26,6 +26,7 @@ import ( "math/big" "net" "net/http" + "regexp" "time" "github.com/fatedier/frp/assets" @@ -45,6 +46,8 @@ import ( "github.com/fatedier/golib/net/mux" fmux "github.com/hashicorp/yamux" + + "github.com/fatedier/frp/extend/api" ) const ( @@ -367,7 +370,35 @@ func (svr *Service) RegisterControl(ctlConn frpNet.Conn, loginMsg *msg.Login) (e err = fmt.Errorf("authorization failed") return } + + if g.GlbServerCfg.EnableApi { + + nowTime := time.Now().Unix() + + s, err := api.NewService(g.GlbServerCfg.ApiBaseUrl) + if err != nil { + return err + } + r := regexp.MustCompile(`^[A-Za-z0-9]{1,32}$`) + mm := r.FindAllStringSubmatch(loginMsg.User, -1) + + if len(mm) < 1 { + return fmt.Errorf("invalid username") + } + + // Connect to API server and verify the user. + valid, err := s.CheckToken(loginMsg.User, loginMsg.PrivilegeKey, nowTime, g.GlbServerCfg.ApiToken) + + if err != nil { + return err + } + + if !valid { + return fmt.Errorf("authorization failed") + } + } + // If client's RunId is empty, it's a new client, we just create a new controller. // Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one. if loginMsg.RunId == "" {