Merge pull request #1080 from fatedier/client

frpc: support admin UI
This commit is contained in:
fatedier 2019-02-11 14:42:48 +08:00 committed by GitHub
commit 1723d7b651
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 16376 additions and 121 deletions

View File

@ -6,11 +6,12 @@ build: frps frpc
# compile assets into binary file # compile assets into binary file
file: file:
rm -rf ./assets/static/* rm -rf ./assets/frps/static/*
cp -rf ./web/frps/dist/* ./assets/static rm -rf ./assets/frpc/static/*
go get -d github.com/rakyll/statik cp -rf ./web/frps/dist/* ./assets/frps/static
go install github.com/rakyll/statik cp -rf ./web/frpc/dist/* ./assets/frpc/static
rm -rf ./assets/statik rm -rf ./assets/frps/statik
rm -rf ./assets/frpc/statik
go generate ./assets/... go generate ./assets/...
fmt: fmt:
@ -18,7 +19,6 @@ fmt:
frps: frps:
go build -o bin/frps ./cmd/frps go build -o bin/frps ./cmd/frps
@cp -rf ./assets/static ./bin
frpc: frpc:
go build -o bin/frpc ./cmd/frpc go build -o bin/frpc ./cmd/frpc

View File

@ -14,8 +14,10 @@
package assets package assets
//go:generate statik -src=./static //go:generate statik -src=./frps/static -dest=./frps
//go:generate go fmt statik/statik.go //go:generate statik -src=./frpc/static -dest=./frpc
//go:generate go fmt ./frps/statik/statik.go
//go:generate go fmt ./frpc/statik/statik.go
import ( import (
"io/ioutil" "io/ioutil"
@ -24,8 +26,6 @@ import (
"path" "path"
"github.com/rakyll/statik/fs" "github.com/rakyll/statik/fs"
_ "github.com/fatedier/frp/assets/statik"
) )
var ( var (

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1 @@
<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frp client admin UI</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?d2cd6337d30c7b22e836"></script><script type="text/javascript" src="vendor.js?edb271e1d9c81f857840"></script></body> </html>

View File

@ -0,0 +1 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(r&&r(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=n(n.s=u[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"edb271e1d9c81f857840"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,u.appendChild(i),c},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/g" "github.com/fatedier/frp/g"
frpNet "github.com/fatedier/frp/utils/net" frpNet "github.com/fatedier/frp/utils/net"
@ -41,6 +42,15 @@ func (svr *Service) RunAdminServer(addr string, port int) (err error) {
// api, see dashboard_api.go // api, see dashboard_api.go
router.HandleFunc("/api/reload", svr.apiReload).Methods("GET") router.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
router.HandleFunc("/api/status", svr.apiStatus).Methods("GET") router.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
router.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
router.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
// view
router.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
router.PathPrefix("/static/").Handler(frpNet.MakeHttpGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
})
address := fmt.Sprintf("%s:%d", addr, port) address := fmt.Sprintf("%s:%d", addr, port)
server := &http.Server{ server := &http.Server{

View File

@ -17,6 +17,7 @@ package client
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"sort" "sort"
"strings" "strings"
@ -28,57 +29,53 @@ import (
) )
type GeneralResponse struct { type GeneralResponse struct {
Code int64 `json:"code"` Code int
Msg string `json:"msg"` Msg string
} }
// api/reload // GET api/reload
type ReloadResp struct {
GeneralResponse
}
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
var ( res := GeneralResponse{Code: 200}
buf []byte
res ReloadResp
)
defer func() {
log.Info("Http response [/api/reload]: code [%d]", res.Code)
buf, _ = json.Marshal(&res)
w.Write(buf)
}()
log.Info("Http request: [/api/reload]") log.Info("Http request [/api/reload]")
defer func() {
log.Info("Http response [/api/reload], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}()
content, err := config.GetRenderedConfFromFile(g.GlbClientCfg.CfgFile) content, err := config.GetRenderedConfFromFile(g.GlbClientCfg.CfgFile)
if err != nil { if err != nil {
res.Code = 1 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Error("reload frpc config file error: %v", err) log.Warn("reload frpc config file error: %s", res.Msg)
return return
} }
newCommonCfg, err := config.UnmarshalClientConfFromIni(nil, content) newCommonCfg, err := config.UnmarshalClientConfFromIni(nil, content)
if err != nil { if err != nil {
res.Code = 2 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Error("reload frpc common section error: %v", err) log.Warn("reload frpc common section error: %s", res.Msg)
return return
} }
pxyCfgs, visitorCfgs, err := config.LoadAllConfFromIni(g.GlbClientCfg.User, content, newCommonCfg.Start) pxyCfgs, visitorCfgs, err := config.LoadAllConfFromIni(g.GlbClientCfg.User, content, newCommonCfg.Start)
if err != nil { if err != nil {
res.Code = 3 res.Code = 400
res.Msg = err.Error() res.Msg = err.Error()
log.Error("reload frpc proxy config error: %v", err) log.Warn("reload frpc proxy config error: %s", res.Msg)
return return
} }
err = svr.ReloadConf(pxyCfgs, visitorCfgs) err = svr.ReloadConf(pxyCfgs, visitorCfgs)
if err != nil { if err != nil {
res.Code = 4 res.Code = 500
res.Msg = err.Error() res.Msg = err.Error()
log.Error("reload frpc proxy config error: %v", err) log.Warn("reload frpc proxy config error: %s", res.Msg)
return return
} }
log.Info("success reload conf") log.Info("success reload conf")
@ -163,7 +160,7 @@ func NewProxyStatusResp(status *proxy.ProxyStatus) ProxyStatusResp {
return psr return psr
} }
// api/status // GET api/status
func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) { func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
var ( var (
buf []byte buf []byte
@ -175,14 +172,14 @@ func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
res.Https = make([]ProxyStatusResp, 0) res.Https = make([]ProxyStatusResp, 0)
res.Stcp = make([]ProxyStatusResp, 0) res.Stcp = make([]ProxyStatusResp, 0)
res.Xtcp = make([]ProxyStatusResp, 0) res.Xtcp = make([]ProxyStatusResp, 0)
log.Info("Http request [/api/status]")
defer func() { defer func() {
log.Info("Http response [/api/status]") log.Info("Http response [/api/status]")
buf, _ = json.Marshal(&res) buf, _ = json.Marshal(&res)
w.Write(buf) w.Write(buf)
}() }()
log.Info("Http request: [/api/status]")
ps := svr.ctl.pm.GetAllProxyStatus() ps := svr.ctl.pm.GetAllProxyStatus()
for _, status := range ps { for _, status := range ps {
switch status.Type { switch status.Type {
@ -208,3 +205,120 @@ func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
sort.Sort(ByProxyStatusResp(res.Xtcp)) sort.Sort(ByProxyStatusResp(res.Xtcp))
return return
} }
// GET api/config
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("Http get request [/api/config]")
defer func() {
log.Info("Http get response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}()
if g.GlbClientCfg.CfgFile == "" {
res.Code = 400
res.Msg = "frpc has no config file path"
log.Warn("%s", res.Msg)
return
}
content, err := config.GetRenderedConfFromFile(g.GlbClientCfg.CfgFile)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("load frpc config file error: %s", res.Msg)
return
}
rows := strings.Split(content, "\n")
newRows := make([]string, 0, len(rows))
for _, row := range rows {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
continue
}
newRows = append(newRows, row)
}
res.Msg = strings.Join(newRows, "\n")
}
// PUT api/config
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("Http put request [/api/config]")
defer func() {
log.Info("Http put response [/api/config], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}()
// get new config content
body, err := ioutil.ReadAll(r.Body)
if err != nil {
res.Code = 400
res.Msg = fmt.Sprintf("read request body error: %v", err)
log.Warn("%s", res.Msg)
return
}
if len(body) == 0 {
res.Code = 400
res.Msg = "body can't be empty"
log.Warn("%s", res.Msg)
return
}
// get token from origin content
token := ""
b, err := ioutil.ReadFile(g.GlbClientCfg.CfgFile)
if err != nil {
res.Code = 400
res.Msg = err.Error()
log.Warn("load frpc config file error: %s", res.Msg)
return
}
content := string(b)
for _, row := range strings.Split(content, "\n") {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
token = row
break
}
}
tmpRows := make([]string, 0)
for _, row := range strings.Split(string(body), "\n") {
row = strings.TrimSpace(row)
if strings.HasPrefix(row, "token") {
continue
}
tmpRows = append(tmpRows, row)
}
newRows := make([]string, 0)
if token != "" {
for _, row := range tmpRows {
newRows = append(newRows, row)
if strings.HasPrefix(row, "[common]") {
newRows = append(newRows, token)
}
}
}
content = strings.Join(newRows, "\n")
err = ioutil.WriteFile(g.GlbClientCfg.CfgFile, []byte(content), 0644)
if err != nil {
res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warn("%s", res.Msg)
return
}
}

View File

@ -22,6 +22,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/fatedier/frp/assets"
"github.com/fatedier/frp/g" "github.com/fatedier/frp/g"
"github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/config"
"github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/models/msg"
@ -49,7 +50,14 @@ type Service struct {
closedCh chan int closedCh chan int
} }
func NewService(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) (svr *Service) { func NewService(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) (svr *Service, err error) {
// Init assets
err = assets.Load("")
if err != nil {
err = fmt.Errorf("Load assets error: %v", err)
return
}
svr = &Service{ svr = &Service{
pxyCfgs: pxyCfgs, pxyCfgs: pxyCfgs,
visitorCfgs: visitorCfgs, visitorCfgs: visitorCfgs,

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package main // "github.com/fatedier/frp/cmd/frpc" package main
import ( import (
_ "github.com/fatedier/frp/assets/frpc/statik"
"github.com/fatedier/frp/cmd/frpc/sub" "github.com/fatedier/frp/cmd/frpc/sub"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"

View File

@ -16,7 +16,6 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -25,7 +24,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/g" "github.com/fatedier/frp/g"
"github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/config"
) )
@ -79,21 +77,16 @@ func reload() error {
if err != nil { if err != nil {
return err return err
} else { } else {
if resp.StatusCode != 200 { if resp.StatusCode == 200 {
return fmt.Errorf("admin api status code [%d]", resp.StatusCode) return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
res := &client.GeneralResponse{} return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body)))
} else if res.Code != 0 {
return fmt.Errorf(res.Msg)
}
} }
return nil return nil
} }

View File

@ -205,7 +205,11 @@ func startService(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]co
}, },
} }
} }
svr := client.NewService(pxyCfgs, visitorCfgs) svr, errRet := client.NewService(pxyCfgs, visitorCfgs)
if errRet != nil {
err = errRet
return
}
// Capture the exit signal if we use kcp. // Capture the exit signal if we use kcp.
if g.GlbClientCfg.Protocol == "kcp" { if g.GlbClientCfg.Protocol == "kcp" {

View File

@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package main // "github.com/fatedier/frp/cmd/frps" package main
import ( import (
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
_ "github.com/fatedier/frp/assets/frps/statik"
) )
func main() { func main() {

View File

@ -28,13 +28,11 @@ import (
) )
type GeneralResponse struct { type GeneralResponse struct {
Code int64 `json:"code"` Code int
Msg string `json:"msg"` Msg string
} }
type ServerInfoResp struct { type ServerInfoResp struct {
GeneralResponse
Version string `json:"version"` Version string `json:"version"`
BindPort int `json:"bind_port"` BindPort int `json:"bind_port"`
BindUdpPort int `json:"bind_udp_port"` BindUdpPort int `json:"bind_udp_port"`
@ -55,18 +53,19 @@ type ServerInfoResp struct {
// api/serverinfo // api/serverinfo
func (svr *Service) ApiServerInfo(w http.ResponseWriter, r *http.Request) { func (svr *Service) ApiServerInfo(w http.ResponseWriter, r *http.Request) {
var ( res := GeneralResponse{Code: 200}
buf []byte
res ServerInfoResp
)
defer func() { defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}() }()
log.Info("Http request: [%s]", r.URL.Path) log.Info("Http request: [%s]", r.URL.Path)
cfg := &g.GlbServerCfg.ServerCommonConf cfg := &g.GlbServerCfg.ServerCommonConf
serverStats := svr.statsCollector.GetServer() serverStats := svr.statsCollector.GetServer()
res = ServerInfoResp{ svrResp := ServerInfoResp{
Version: version.Full(), Version: version.Full(),
BindPort: cfg.BindPort, BindPort: cfg.BindPort,
BindUdpPort: cfg.BindUdpPort, BindUdpPort: cfg.BindUdpPort,
@ -85,8 +84,8 @@ func (svr *Service) ApiServerInfo(w http.ResponseWriter, r *http.Request) {
ProxyTypeCounts: serverStats.ProxyTypeCounts, ProxyTypeCounts: serverStats.ProxyTypeCounts,
} }
buf, _ = json.Marshal(&res) buf, _ := json.Marshal(&svrResp)
w.Write(buf) res.Msg = string(buf)
} }
type BaseOutConf struct { type BaseOutConf struct {
@ -155,31 +154,29 @@ type ProxyStatsInfo struct {
} }
type GetProxyInfoResp struct { type GetProxyInfoResp struct {
GeneralResponse
Proxies []*ProxyStatsInfo `json:"proxies"` Proxies []*ProxyStatsInfo `json:"proxies"`
} }
// api/proxy/:type // api/proxy/:type
func (svr *Service) ApiProxyByType(w http.ResponseWriter, r *http.Request) { func (svr *Service) ApiProxyByType(w http.ResponseWriter, r *http.Request) {
var ( res := GeneralResponse{Code: 200}
buf []byte
res GetProxyInfoResp
)
params := mux.Vars(r) params := mux.Vars(r)
proxyType := params["type"] proxyType := params["type"]
defer func() { defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
log.Info(r.URL.Path) w.WriteHeader(res.Code)
log.Info(r.URL.RawPath) if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}() }()
log.Info("Http request: [%s]", r.URL.Path) log.Info("Http request: [%s]", r.URL.Path)
res.Proxies = svr.getProxyStatsByType(proxyType) proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
buf, _ = json.Marshal(&res)
w.Write(buf)
buf, _ := json.Marshal(&proxyInfoResp)
res.Msg = string(buf)
} }
func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) { func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
@ -215,8 +212,6 @@ func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxySt
// Get proxy info by name. // Get proxy info by name.
type GetProxyStatsResp struct { type GetProxyStatsResp struct {
GeneralResponse
Name string `json:"name"` Name string `json:"name"`
Conf interface{} `json:"conf"` Conf interface{} `json:"conf"`
TodayTrafficIn int64 `json:"today_traffic_in"` TodayTrafficIn int64 `json:"today_traffic_in"`
@ -229,45 +224,50 @@ type GetProxyStatsResp struct {
// api/proxy/:type/:name // api/proxy/:type/:name
func (svr *Service) ApiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) { func (svr *Service) ApiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
var ( res := GeneralResponse{Code: 200}
buf []byte
res GetProxyStatsResp
)
params := mux.Vars(r) params := mux.Vars(r)
proxyType := params["type"] proxyType := params["type"]
name := params["name"] name := params["name"]
defer func() { defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}() }()
log.Info("Http request: [%s]", r.URL.Path) log.Info("Http request: [%s]", r.URL.Path)
res = svr.getProxyStatsByTypeAndName(proxyType, name) proxyStatsResp := GetProxyStatsResp{}
proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name)
if res.Code != 200 {
return
}
buf, _ = json.Marshal(&res) buf, _ := json.Marshal(&proxyStatsResp)
w.Write(buf) res.Msg = string(buf)
} }
func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp) { func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
proxyInfo.Name = proxyName proxyInfo.Name = proxyName
ps := svr.statsCollector.GetProxiesByTypeAndName(proxyType, proxyName) ps := svr.statsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
if ps == nil { if ps == nil {
proxyInfo.Code = 1 code = 404
proxyInfo.Msg = "no proxy info found" msg = "no proxy info found"
} else { } else {
if pxy, ok := svr.pxyManager.GetByName(proxyName); ok { if pxy, ok := svr.pxyManager.GetByName(proxyName); ok {
content, err := json.Marshal(pxy.GetConf()) content, err := json.Marshal(pxy.GetConf())
if err != nil { if err != nil {
log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err) log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err)
proxyInfo.Code = 2 code = 400
proxyInfo.Msg = "parse conf error" msg = "parse conf error"
return return
} }
proxyInfo.Conf = getConfByType(ps.Type) proxyInfo.Conf = getConfByType(ps.Type)
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil { if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
log.Warn("unmarshal proxy [%s] conf info error: %v", ps.Name, err) log.Warn("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
proxyInfo.Code = 2 code = 400
proxyInfo.Msg = "parse conf error" msg = "parse conf error"
return return
} }
proxyInfo.Status = consts.Online proxyInfo.Status = consts.Online
@ -286,36 +286,38 @@ func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName strin
// api/traffic/:name // api/traffic/:name
type GetProxyTrafficResp struct { type GetProxyTrafficResp struct {
GeneralResponse
Name string `json:"name"` Name string `json:"name"`
TrafficIn []int64 `json:"traffic_in"` TrafficIn []int64 `json:"traffic_in"`
TrafficOut []int64 `json:"traffic_out"` TrafficOut []int64 `json:"traffic_out"`
} }
func (svr *Service) ApiProxyTraffic(w http.ResponseWriter, r *http.Request) { func (svr *Service) ApiProxyTraffic(w http.ResponseWriter, r *http.Request) {
var ( res := GeneralResponse{Code: 200}
buf []byte
res GetProxyTrafficResp
)
params := mux.Vars(r) params := mux.Vars(r)
name := params["name"] name := params["name"]
defer func() { defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code) log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
w.Write([]byte(res.Msg))
}
}() }()
log.Info("Http request: [%s]", r.URL.Path) log.Info("Http request: [%s]", r.URL.Path)
res.Name = name trafficResp := GetProxyTrafficResp{}
trafficResp.Name = name
proxyTrafficInfo := svr.statsCollector.GetProxyTraffic(name) proxyTrafficInfo := svr.statsCollector.GetProxyTraffic(name)
if proxyTrafficInfo == nil { if proxyTrafficInfo == nil {
res.Code = 1 res.Code = 404
res.Msg = "no proxy info found" res.Msg = "no proxy info found"
return
} else { } else {
res.TrafficIn = proxyTrafficInfo.TrafficIn trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
res.TrafficOut = proxyTrafficInfo.TrafficOut trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
} }
buf, _ = json.Marshal(&res) buf, _ := json.Marshal(&trafficResp)
w.Write(buf) res.Msg = string(buf)
} }

View File

@ -89,7 +89,7 @@ func NewService() (svr *Service, err error) {
// Init group controller // Init group controller
svr.rc.TcpGroupCtl = group.NewTcpGroupCtl(svr.rc.TcpPortManager) svr.rc.TcpGroupCtl = group.NewTcpGroupCtl(svr.rc.TcpPortManager)
// Init assets. // Init assets
err = assets.Load(cfg.AssetsDir) err = assets.Load(cfg.AssetsDir)
if err != nil { if err != nil {
err = fmt.Errorf("Load assets error: %v", err) err = fmt.Errorf("Load assets error: %v", err)

View File

@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
var version string = "0.23.3" var version string = "0.24.0"
func Full() string { func Full() string {
return version return version

14
web/frpc/.babelrc Normal file
View File

@ -0,0 +1,14 @@
{
"presets": [
["es2015", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

6
web/frpc/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.DS_Store
node_modules/
dist/
npm-debug.log
.idea
.vscode/settings.json

6
web/frpc/Makefile Normal file
View File

@ -0,0 +1,6 @@
.PHONY: dist build
build:
@npm run build
dev:
@npm run dev

9334
web/frpc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
web/frpc/package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "frpc-web",
"description": "An admin web ui for frp client.",
"author": "fatedier",
"private": true,
"scripts": {
"dev": "webpack-dev-server -d --inline --hot --env.dev",
"build": "rimraf dist && webpack -p --progress --hide-modules"
},
"dependencies": {
"element-ui": "^2.5.3",
"vue": "^2.5.22",
"vue-resource": "^1.5.1",
"vue-router": "^3.0.2",
"whatwg-fetch": "^3.0.0"
},
"engines": {
"node": ">=6"
},
"devDependencies": {
"autoprefixer": "^9.4.7",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-component": "^1.1.1",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^2.1.0",
"eslint": "^5.12.1",
"eslint-config-enough": "^0.3.4",
"eslint-loader": "^2.1.1",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.24.1",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"postcss-loader": "^3.0.0",
"rimraf": "^2.6.3",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"vue-loader": "^15.6.2",
"vue-template-compiler": "^2.5.22",
"webpack": "^2.7.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')()
]
}

73
web/frpc/src/App.vue Normal file
View File

@ -0,0 +1,73 @@
<template>
<div id="app">
<header class="grid-content header-color">
<el-row>
<a class="brand" href="#">frp client</a>
</el-row>
</header>
<section>
<el-row :gutter="20">
<el-col id="side-nav" :xs="24" :md="4">
<el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect">
<el-menu-item index="/">Overview</el-menu-item>
<el-menu-item index="/configure">Configure</el-menu-item>
<el-menu-item index="">Help</el-menu-item>
</el-menu>
</el-col>
<el-col :xs="24" :md="20">
<div id="content">
<router-view></router-view>
</div>
</el-col>
</el-row>
</section>
<footer></footer>
</div>
</template>
<script>
export default {
methods: {
handleSelect(key, path) {
if (key == '') {
window.open("https://github.com/fatedier/frp")
}
}
}
}
</script>
<style>
body {
background-color: #fafafa;
margin: 0px;
font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif;
}
header {
width: 100%;
height: 60px;
}
.header-color {
background: #58B7FF;
}
#content {
margin-top: 20px;
padding-right: 40px;
}
.brand {
color: #fff;
background-color: transparent;
margin-left: 20px;
float: left;
line-height: 25px;
font-size: 25px;
padding: 15px 15px;
height: 30px;
text-decoration: none;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,93 @@
<template>
<div>
<el-row id="head">
<el-button type="primary" @click="fetchData">Refresh</el-button>
<el-button type="primary" @click="uploadConfig">Upload</el-button>
</el-row>
<el-input type="textarea" autosize v-model="textarea" placeholder="frpc configrue file, can not be empty..."></el-input>
</div>
</template>
<script>
export default {
data() {
return {
textarea: ''
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData() {
fetch('/api/config', {credentials: 'include'})
.then(res => {
return res.text()
}).then(text => {
this.textarea= text
}).catch( err => {
this.$message({
showClose: true,
message: 'Get configure content from frpc failed!',
type: 'warning'
})
})
},
uploadConfig() {
this.$confirm('This operation will upload your frpc configure file content and hot reload it, do you want to continue?', 'Notice', {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
type: 'warning'
}).then(() => {
if (this.textarea == "") {
this.$message({
type: 'warning',
message: 'Configure content can not be empty!'
})
return
}
fetch('/api/config', {
credentials: 'include',
method: 'PUT',
body: this.textarea,
}).then(() => {
fetch('/api/reload', {credentials: 'include'})
.then(() => {
this.$message({
type: 'success',
message: 'Success'
})
}).catch(err => {
this.$message({
showClose: true,
message: 'Reload frpc configure file error, ' + err,
type: 'warning'
})
})
}).catch(err => {
this.$message({
showClose: true,
message: 'Put config to frpc and hot reload failed!',
type: 'warning'
})
})
}).catch(() => {
this.$message({
type: 'info',
message: 'Canceled'
})
})
}
}
}
</script>
<style>
#head {
margin-bottom: 30px;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div>
<el-row>
<el-col :md="24">
<div>
<el-table :data="status" stripe style="width: 100%" :default-sort="{prop: 'type', order: 'ascending'}">
<el-table-column prop="name" label="name"></el-table-column>
<el-table-column prop="type" label="type" width="150"></el-table-column>
<el-table-column prop="local_addr" label="local address" width="200"></el-table-column>
<el-table-column prop="plugin" label="plugin" width="200"></el-table-column>
<el-table-column prop="remote_addr" label="remote address"></el-table-column>
<el-table-column prop="status" label="status" width="150"></el-table-column>
<el-table-column prop="err" label="info"></el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
data() {
return {
status: null
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData() {
fetch('/api/status', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.status = new Array()
for (let s of json.tcp) {
this.status.push(s)
}
for (let s of json.udp) {
this.status.push(s)
}
for (let s of json.http) {
this.status.push(s)
}
for (let s of json.https) {
this.status.push(s)
}
for (let s of json.stcp) {
this.status.push(s)
}
for (let s of json.xtcp) {
this.status.push(s)
}
}).catch( err => {
this.$message({
showClose: true,
message: 'Get status info from frpc failed!',
type: 'warning'
})
})
}
}
}
</script>
<style>
</style>

15
web/frpc/src/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>frp client admin UI</title>
</head>
<body>
<div id="app"></div>
<!--<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>-->
<!--<script src="//cdn.bootcss.com/echarts/3.4.0/echarts.min.js"></script>-->
</body>
</html>

52
web/frpc/src/main.js Normal file
View File

@ -0,0 +1,52 @@
import Vue from 'vue'
// import ElementUI from 'element-ui'
import {
Button,
Form,
FormItem,
Row,
Col,
Table,
TableColumn,
Menu,
MenuItem,
MessageBox,
Message,
Input
} from 'element-ui'
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
import 'element-ui/lib/theme-chalk/index.css'
import './utils/less/custom.less'
import App from './App.vue'
import router from './router'
import 'whatwg-fetch'
locale.use(lang)
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Input)
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$message = Message
//Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})

View File

@ -0,0 +1,18 @@
import Vue from 'vue'
import Router from 'vue-router'
import Overview from '../components/Overview.vue'
import Configure from '../components/Configure.vue'
Vue.use(Router)
export default new Router({
routes: [{
path: '/',
name: 'Overview',
component: Overview
},{
path: '/configure',
name: 'Configure',
component: Configure,
}]
})

View File

@ -0,0 +1,22 @@
@color: red;
.el-form-item {
span {
margin-left: 15px;
}
}
.demo-table-expand {
font-size: 0;
label {
width: 90px;
color: #99a9bf;
}
.el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
}

View File

@ -0,0 +1,13 @@
class ProxyStatus {
constructor(status) {
this.name = status.name
this.type = status.type
this.status = status.status
this.err = status.err
this.local_addr = status.local_addr
this.plugin = status.plugin
this.remote_addr = status.remote_addr
}
}
export {ProxyStatus}

107
web/frpc/webpack.config.js Normal file
View File

@ -0,0 +1,107 @@
const path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var url = require('url')
var publicPath = ''
module.exports = (options = {}) => ({
entry: {
vendor: './src/main'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: options.dev ? '[name].js' : '[name].js?[chunkhash]',
chunkFilename: '[id].js?[chunkhash]',
publicPath: options.dev ? '/assets/' : publicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': path.resolve(__dirname, 'src'),
}
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
}, {
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
root: path.resolve(__dirname, 'src'),
attrs: ['img:src', 'link:href']
}
}]
}, {
test: /\.less$/,
loader: 'style-loader!css-loader!postcss-loader!less-loader'
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /favicon\.png$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}]
}, {
test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
exclude: /favicon\.png$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000
}
}]
}]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest']
}),
new HtmlWebpackPlugin({
favicon: 'src/assets/favicon.ico',
template: 'src/index.html'
}),
new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: false,
comments: false,
compress: {
warnings: false
}
}),
new VueLoaderPlugin()
],
devServer: {
host: '127.0.0.1',
port: 8010,
proxy: {
'/api/': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
historyApiFallback: {
index: url.parse(options.dev ? '/assets/' : publicPath).pathname
}
}//,
//devtool: options.dev ? '#eval-source-map' : '#source-map'
})

6236
web/frpc/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,7 @@
.PHONY: dist build .PHONY: dist build
install:
@npm install build:
@npm run build
dev: install dev: install
@npm run dev @npm run dev
build:
@npm run build