mirror of
https://github.com/fatedier/frp.git
synced 2026-07-29 06:39:19 +08:00
Compare commits
21 Commits
v0.65.0
...
a4175a2595
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4175a2595 | ||
|
|
36718d88e4 | ||
|
|
bc378bcbec | ||
|
|
33428ab538 | ||
|
|
ef96481f58 | ||
|
|
7526d7a69a | ||
|
|
2bdf25bae6 | ||
|
|
0fe8f7a0b6 | ||
|
|
2e2802ea13 | ||
|
|
c3821202b1 | ||
|
|
15fd19a16d | ||
|
|
66973a03db | ||
|
|
f736d171ac | ||
|
|
b27b846971 | ||
|
|
e025843d3c | ||
|
|
a75320ef2f | ||
|
|
1cf325bb0c | ||
|
|
469097a549 | ||
|
|
2def23bb0b | ||
|
|
ee3cc4b14e | ||
|
|
e382676659 |
@@ -39,6 +39,7 @@ linters:
|
|||||||
- G404
|
- G404
|
||||||
- G501
|
- G501
|
||||||
- G115
|
- G115
|
||||||
|
- G204
|
||||||
severity: low
|
severity: low
|
||||||
confidence: low
|
confidence: low
|
||||||
govet:
|
govet:
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -13,13 +13,6 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp" target="_blank">
|
|
||||||
<b>Recall.ai - API for meeting recordings</b><br>
|
|
||||||
<br>
|
|
||||||
<sup>If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.</sup>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://go.warp.dev/frp" target="_blank">
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
@@ -29,6 +22,7 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<sub>Available for macOS, Linux and Windows</sub>
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
@@ -36,13 +30,7 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
|
||||||
<br>
|
|
||||||
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
@@ -52,6 +40,25 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
|
|||||||
40
README_zh.md
40
README_zh.md
@@ -15,13 +15,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
|
||||||
<a href="https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp" target="_blank">
|
|
||||||
<b>Recall.ai - API for meeting recordings</b><br>
|
|
||||||
<br>
|
|
||||||
<sup>If you're looking for a meeting recording API, consider checking out Recall.ai, an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.</sup>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://go.warp.dev/frp" target="_blank">
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
@@ -31,6 +24,7 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
<sub>Available for macOS, Linux and Windows</sub>
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
@@ -38,13 +32,7 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
<b>The complete IDE crafted for professional Go developers</b>
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
|
||||||
<br>
|
|
||||||
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
@@ -54,6 +42,24 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
@@ -126,9 +132,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
|||||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||||
|
|
||||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||||
|
|
||||||
### 知识星球
|
|
||||||
|
|
||||||
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Add NAT traversal configuration options for XTCP proxies and visitors. Support disabling assisted addresses to avoid using slow VPN connections during NAT hole punching.
|
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
||||||
* Enhanced OIDC client configuration with support for custom TLS certificate verification and proxy settings. Added `trustedCaFile`, `insecureSkipVerify`, and `proxyURL` options for OIDC token endpoint connections.
|
* Redesigned the frps web dashboard with a modern UI, dark mode support, and improved navigation.
|
||||||
* Added detailed Prometheus metrics with `proxy_counts_detailed` metric that includes both proxy type and proxy name labels, enabling monitoring of individual proxy connections instead of just aggregate counts.
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>frp client admin UI</title>
|
<title>frp client admin UI</title>
|
||||||
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
|
<script type="module" crossorigin src="./index-HyKZ_pht.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
|
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1
assets/frps/static/index-BUrDiw1t.js
Normal file
1
assets/frps/static/index-BUrDiw1t.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/frps/static/index-D4KRVvIu.css
Normal file
1
assets/frps/static/index-D4KRVvIu.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>frps dashboard</title>
|
<title>frp server</title>
|
||||||
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
|
<script type="module" crossorigin src="./index-BUrDiw1t.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
|
<link rel="stylesheet" crossorigin href="./index-D4KRVvIu.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
|
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil {
|
||||||
res.Code = 400
|
res.Code = 400
|
||||||
res.Msg = err.Error()
|
res.Msg = err.Error()
|
||||||
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ type SessionContext struct {
|
|||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
// Indicates whether the connection is encrypted.
|
// Indicates whether the connection is encrypted.
|
||||||
ConnEncrypted bool
|
ConnEncrypted bool
|
||||||
// Sets authentication based on selected method
|
// Auth runtime used for login, heartbeats, and encryption.
|
||||||
AuthSetter auth.Setter
|
Auth *auth.ClientAuth
|
||||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||||
Connector Connector
|
Connector Connector
|
||||||
// Virtual net controller
|
// Virtual net controller
|
||||||
@@ -91,7 +91,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.lastPong.Store(time.Now())
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
if sessionCtx.ConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,9 +100,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||||
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
@@ -133,7 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
m := &msg.NewWorkConn{
|
m := &msg.NewWorkConn{
|
||||||
RunID: ctl.sessionCtx.RunID,
|
RunID: ctl.sessionCtx.RunID,
|
||||||
}
|
}
|
||||||
if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
|
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
||||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
@@ -243,7 +243,7 @@ func (ctl *Control) heartbeatWorker() {
|
|||||||
sendHeartBeat := func() (bool, error) {
|
sendHeartBeat := func() (bool, error) {
|
||||||
xl.Debugf("send heartbeat to server")
|
xl.Debugf("send heartbeat to server")
|
||||||
pingMsg := &msg.Ping{}
|
pingMsg := &msg.Ping{}
|
||||||
if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
|
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
||||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func NewProxy(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pxyConf v1.ProxyConfigurer,
|
pxyConf v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) (pxy Proxy) {
|
) (pxy Proxy) {
|
||||||
@@ -69,6 +70,7 @@ func NewProxy(
|
|||||||
baseProxy := BaseProxy{
|
baseProxy := BaseProxy{
|
||||||
baseCfg: pxyConf.GetBaseConfig(),
|
baseCfg: pxyConf.GetBaseConfig(),
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
@@ -86,6 +88,7 @@ func NewProxy(
|
|||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
baseCfg *v1.ProxyBaseConfig
|
baseCfg *v1.ProxyBaseConfig
|
||||||
clientCfg *v1.ClientCommonConfig
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
encryptionKey []byte
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
@@ -129,7 +132,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
|
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common handler for tcp work connections.
|
// Common handler for tcp work connections.
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ type Manager struct {
|
|||||||
closed bool
|
closed bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
clientCfg *v1.ClientCommonConfig
|
encryptionKey []byte
|
||||||
|
clientCfg *v1.ClientCommonConfig
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,7 @@ type Manager struct {
|
|||||||
func NewManager(
|
func NewManager(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
@@ -56,6 +58,7 @@ func NewManager(
|
|||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
closed: false,
|
closed: false,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
|||||||
for _, cfg := range proxyCfgs {
|
for _, cfg := range proxyCfgs {
|
||||||
name := cfg.GetBaseConfig().Name
|
name := cfg.GetBaseConfig().Name
|
||||||
if _, ok := pm.proxies[name]; !ok {
|
if _, ok := pm.proxies[name]; !ok {
|
||||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||||
if pm.inWorkConnCallback != nil {
|
if pm.inWorkConnCallback != nil {
|
||||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ func NewWrapper(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg v1.ProxyConfigurer,
|
cfg v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
|
encryptionKey []byte,
|
||||||
eventHandler event.Handler,
|
eventHandler event.Handler,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
@@ -122,7 +123,7 @@ func NewWrapper(
|
|||||||
xl.Tracef("enable health check monitor")
|
xl.Tracef("enable health check monitor")
|
||||||
}
|
}
|
||||||
|
|
||||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
|
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
||||||
return pw
|
return pw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/auth"
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -64,6 +65,8 @@ type ServiceOptions struct {
|
|||||||
ProxyCfgs []v1.ProxyConfigurer
|
ProxyCfgs []v1.ProxyConfigurer
|
||||||
VisitorCfgs []v1.VisitorConfigurer
|
VisitorCfgs []v1.VisitorConfigurer
|
||||||
|
|
||||||
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||||
// If it is empty, it means that the configuration file is not used for initialization.
|
// If it is empty, it means that the configuration file is not used for initialization.
|
||||||
// It may be initialized using command line parameters or called directly.
|
// It may be initialized using command line parameters or called directly.
|
||||||
@@ -108,8 +111,8 @@ type Service struct {
|
|||||||
// Uniq id got from frps, it will be attached to loginMsg.
|
// Uniq id got from frps, it will be attached to loginMsg.
|
||||||
runID string
|
runID string
|
||||||
|
|
||||||
// Sets authentication based on selected method
|
// Auth runtime and encryption materials
|
||||||
authSetter auth.Setter
|
auth *auth.ClientAuth
|
||||||
|
|
||||||
// web server for admin UI and apis
|
// web server for admin UI and apis
|
||||||
webServer *httppkg.Server
|
webServer *httppkg.Server
|
||||||
@@ -122,6 +125,8 @@ type Service struct {
|
|||||||
visitorCfgs []v1.VisitorConfigurer
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
clientSpec *msg.ClientSpec
|
clientSpec *msg.ClientSpec
|
||||||
|
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
// The configuration file used to initialize this client, or an empty
|
// The configuration file used to initialize this client, or an empty
|
||||||
// string if no configuration file was used.
|
// string if no configuration file was used.
|
||||||
configFilePath string
|
configFilePath string
|
||||||
@@ -150,17 +155,18 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
webServer = ws
|
webServer = ws
|
||||||
}
|
}
|
||||||
|
|
||||||
authSetter, err := auth.NewAuthSetter(options.Common.Auth)
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
authSetter: authSetter,
|
auth: authRuntime,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
|
unsafeFeatures: options.UnsafeFeatures,
|
||||||
proxyCfgs: options.ProxyCfgs,
|
proxyCfgs: options.ProxyCfgs,
|
||||||
visitorCfgs: options.VisitorCfgs,
|
visitorCfgs: options.VisitorCfgs,
|
||||||
clientSpec: options.ClientSpec,
|
clientSpec: options.ClientSpec,
|
||||||
@@ -275,11 +281,15 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
loginMsg := &msg.Login{
|
loginMsg := &msg.Login{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
|
Hostname: hostname,
|
||||||
PoolCount: svr.common.Transport.PoolCount,
|
PoolCount: svr.common.Transport.PoolCount,
|
||||||
User: svr.common.User,
|
User: svr.common.User,
|
||||||
|
ClientID: svr.common.ClientID,
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
@@ -290,7 +300,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add auth
|
// Add auth
|
||||||
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +354,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ConnEncrypted: connEncrypted,
|
ConnEncrypted: connEncrypted,
|
||||||
AuthSetter: svr.authSetter,
|
Auth: svr.auth,
|
||||||
Connector: connector,
|
Connector: connector,
|
||||||
VnetController: svr.vnetController,
|
VnetController: svr.vnetController,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -81,11 +82,22 @@ func (sv *STCPVisitor) internalConnWorker() {
|
|||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
defer userConn.Close()
|
var tunnelErr error
|
||||||
|
defer func() {
|
||||||
|
// If there was an error and connection supports CloseWithError, use it
|
||||||
|
if tunnelErr != nil {
|
||||||
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
@@ -102,6 +114,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +123,14 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +140,7 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func NewVisitor(
|
|||||||
Name: cfg.GetBaseConfig().Name,
|
Name: cfg.GetBaseConfig().Name,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
VnetController: helper.VNetController(),
|
VnetController: helper.VNetController(),
|
||||||
HandleConn: func(conn net.Conn) {
|
SendConnToVisitor: func(conn net.Conn) {
|
||||||
_ = baseVisitor.AcceptConn(conn)
|
_ = baseVisitor.AcceptConn(conn)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -162,8 +162,16 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
|||||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
isConnTransferred := false
|
isConnTransferred := false
|
||||||
|
var tunnelErr error
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isConnTransferred {
|
if !isConnTransferred {
|
||||||
|
// If there was an error and connection supports CloseWithError, use it
|
||||||
|
if tunnelErr != nil {
|
||||||
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
userConn.Close()
|
userConn.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -181,6 +189,8 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
tunnelConn, err := sv.openTunnel(ctx)
|
tunnelConn, err := sv.openTunnel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("open tunnel error: %v", err)
|
xl.Errorf("open tunnel error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
|
||||||
// no fallback, just return
|
// no fallback, just return
|
||||||
if sv.cfg.FallbackTo == "" {
|
if sv.cfg.FallbackTo == "" {
|
||||||
return
|
return
|
||||||
@@ -200,6 +210,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
var proxyTypes = []v1.ProxyType{
|
var proxyTypes = []v1.ProxyType{
|
||||||
@@ -77,7 +78,10 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -88,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -106,7 +110,9 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -117,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,7 +32,8 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/featuregate"
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
)
|
)
|
||||||
@@ -41,6 +43,7 @@ var (
|
|||||||
cfgDir string
|
cfgDir string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -48,6 +51,9 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -59,15 +65,17 @@ var rootCmd = &cobra.Command{
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
|
||||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||||
if cfgDir != "" {
|
if cfgDir != "" {
|
||||||
_ = runMultipleClients(cfgDir)
|
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not show command usage here.
|
// Do not show command usage here.
|
||||||
err := runClient(cfgFile)
|
err := runClient(cfgFile, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -76,7 +84,7 @@ var rootCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMultipleClients(cfgDir string) error {
|
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil || d.IsDir() {
|
if err != nil || d.IsDir() {
|
||||||
@@ -86,7 +94,7 @@ func runMultipleClients(cfgDir string) error {
|
|||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := runClient(path)
|
err := runClient(path, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||||
}
|
}
|
||||||
@@ -111,7 +119,7 @@ func handleTermSignal(svr *client.Service) {
|
|||||||
svr.GracefulClose(500 * time.Millisecond)
|
svr.GracefulClose(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient(cfgFilePath string) error {
|
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -127,20 +135,22 @@ func runClient(cfgFilePath string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
|
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
|
|
||||||
|
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
func startService(
|
||||||
cfg *v1.ClientCommonConfig,
|
cfg *v1.ClientCommonConfig,
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
cfgFile string,
|
cfgFile string,
|
||||||
) error {
|
) error {
|
||||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
@@ -153,6 +163,7 @@ func startService(
|
|||||||
Common: cfg,
|
Common: cfg,
|
||||||
ProxyCfgs: proxyCfgs,
|
ProxyCfgs: proxyCfgs,
|
||||||
VisitorCfgs: visitorCfgs,
|
VisitorCfgs: visitorCfgs,
|
||||||
|
UnsafeFeatures: unsafeFeatures,
|
||||||
ConfigFilePath: cfgFile,
|
ConfigFilePath: cfgFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -42,7 +43,8 @@ var verifyCmd = &cobra.Command{
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/server"
|
"github.com/fatedier/frp/server"
|
||||||
@@ -33,6 +35,7 @@ var (
|
|||||||
cfgFile string
|
cfgFile string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
|
allowUnsafe []string
|
||||||
|
|
||||||
serverCfg v1.ServerConfig
|
serverCfg v1.ServerConfig
|
||||||
)
|
)
|
||||||
@@ -41,6 +44,8 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||||
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
||||||
|
|
||||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||||
}
|
}
|
||||||
@@ -77,7 +82,9 @@ var rootCmd = &cobra.Command{
|
|||||||
svrCfg = &serverCfg
|
svrCfg = &serverCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -42,7 +43,9 @@ var verifyCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateServerConfig(svrCfg)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
validator := validation.NewConfigValidator(unsafeFeatures)
|
||||||
|
warning, err := validator.ValidateServerConfig(svrCfg)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||||
|
|
||||||
|
# Optional unique identifier for this frpc instance.
|
||||||
|
clientID = "your_client_id"
|
||||||
# your proxy name will be changed to {user}.{proxy}
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
user = "your_name"
|
user = "your_name"
|
||||||
|
|
||||||
@@ -143,6 +145,11 @@ transport.tls.enable = true
|
|||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
# start = ["ssh", "dns"]
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
|
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||||
|
# Set 'enabled = false' in a proxy configuration to disable it.
|
||||||
|
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
||||||
|
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
||||||
|
|
||||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
# This parameter should be same between client and server.
|
# This parameter should be same between client and server.
|
||||||
# It affects the udp and sudp proxy.
|
# It affects the udp and sudp proxy.
|
||||||
@@ -169,6 +176,8 @@ metadatas.var2 = "123"
|
|||||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
name = "ssh"
|
name = "ssh"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
|
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
||||||
|
# enabled = true
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 22
|
localPort = 22
|
||||||
# Limit bandwidth for this proxy, unit is KB and MB
|
# Limit bandwidth for this proxy, unit is KB and MB
|
||||||
@@ -253,6 +262,8 @@ healthCheck.httpHeaders=[
|
|||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "web02"
|
name = "web02"
|
||||||
type = "https"
|
type = "https"
|
||||||
|
# Disable this proxy by setting enabled to false
|
||||||
|
# enabled = false
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 8000
|
localPort = 8000
|
||||||
subdomain = "web02"
|
subdomain = "web02"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
BIN
doc/pic/zsxq.jpg
BIN
doc/pic/zsxq.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
17
go.mod
17
go.mod
@@ -16,7 +16,7 @@ require (
|
|||||||
github.com/pion/stun/v2 v2.0.0
|
github.com/pion/stun/v2 v2.0.0
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/prometheus/client_golang v1.19.1
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/quic-go/quic-go v0.53.0
|
github.com/quic-go/quic-go v0.55.0
|
||||||
github.com/rodaine/table v1.2.0
|
github.com/rodaine/table v1.2.0
|
||||||
github.com/samber/lo v1.47.0
|
github.com/samber/lo v1.47.0
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||||
@@ -26,10 +26,10 @@ require (
|
|||||||
github.com/tidwall/gjson v1.17.1
|
github.com/tidwall/gjson v1.17.1
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.41.0
|
||||||
golang.org/x/net v0.39.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
golang.org/x/sync v0.13.0
|
golang.org/x/sync v0.16.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
@@ -67,11 +67,10 @@ require (
|
|||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|||||||
40
go.sum
40
go.sum
@@ -105,8 +105,8 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
|
|||||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
||||||
@@ -156,24 +156,24 @@ github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -187,8 +187,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
@@ -197,8 +197,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -213,24 +213,24 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -241,8 +241,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -27,14 +28,51 @@ type Setter interface {
|
|||||||
SetNewWorkConn(*msg.NewWorkConn) error
|
SetNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientAuth struct {
|
||||||
|
Setter Setter
|
||||||
|
key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ClientAuth) EncryptionKey() []byte {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||||
|
// Caller must run validation before calling this function.
|
||||||
|
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("auth config is nil")
|
||||||
|
}
|
||||||
|
resolved := *cfg
|
||||||
|
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||||
|
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
resolved.Token = token
|
||||||
|
}
|
||||||
|
setter, err := NewAuthSetter(resolved)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ClientAuth{
|
||||||
|
Setter: setter,
|
||||||
|
key: []byte(resolved.Token),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||||
case v1.AuthMethodOIDC:
|
case v1.AuthMethodOIDC:
|
||||||
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
if cfg.OIDC.TokenSource != nil {
|
||||||
if err != nil {
|
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
|
||||||
return nil, err
|
} else {
|
||||||
|
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
||||||
@@ -48,6 +86,35 @@ type Verifier interface {
|
|||||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerAuth struct {
|
||||||
|
Verifier Verifier
|
||||||
|
key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerAuth) EncryptionKey() []byte {
|
||||||
|
return a.key
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
||||||
|
// Caller must run validation before calling this function.
|
||||||
|
func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("auth config is nil")
|
||||||
|
}
|
||||||
|
resolved := *cfg
|
||||||
|
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
||||||
|
token, err := resolved.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
resolved.Token = token
|
||||||
|
}
|
||||||
|
return &ServerAuth{
|
||||||
|
Verifier: NewAuthVerifier(resolved),
|
||||||
|
key: []byte(resolved.Token),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
|
|||||||
@@ -152,6 +152,51 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OidcTokenSourceAuthProvider struct {
|
||||||
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
|
valueSource *v1.ValueSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
|
||||||
|
return &OidcTokenSourceAuthProvider{
|
||||||
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
|
valueSource: valueSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
accessToken, err = auth.valueSource.Resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||||
|
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||||
|
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
||||||
|
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type TokenVerifier interface {
|
type TokenVerifier interface {
|
||||||
Verify(context.Context, string) (*oidc.IDToken, error)
|
Verify(context.Context, string) (*oidc.IDToken, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
|||||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||||
|
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
|
||||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,17 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by enabled field in each proxy
|
||||||
|
// nil or true means enabled, false means disabled
|
||||||
|
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||||
|
enabled := c.GetBaseConfig().Enabled
|
||||||
|
return enabled == nil || *enabled
|
||||||
|
})
|
||||||
|
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
||||||
|
enabled := c.GetBaseConfig().Enabled
|
||||||
|
return enabled == nil || *enabled
|
||||||
|
})
|
||||||
|
|
||||||
if cliCfg != nil {
|
if cliCfg != nil {
|
||||||
if err := cliCfg.Complete(); err != nil {
|
if err := cliCfg.Complete(); err != nil {
|
||||||
return nil, nil, nil, isLegacyFormat, err
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -39,6 +37,8 @@ type ClientCommonConfig struct {
|
|||||||
// clients. If this value is not "", proxy names will automatically be
|
// clients. If this value is not "", proxy names will automatically be
|
||||||
// changed to "{user}.{proxy_name}".
|
// changed to "{user}.{proxy_name}".
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
|
// ClientID uniquely identifies this frpc instance.
|
||||||
|
ClientID string `json:"clientID,omitempty"`
|
||||||
|
|
||||||
// ServerAddr specifies the address of the server to connect to. By
|
// ServerAddr specifies the address of the server to connect to. By
|
||||||
// default, this value is "0.0.0.0".
|
// default, this value is "0.0.0.0".
|
||||||
@@ -198,17 +198,6 @@ type AuthClientConfig struct {
|
|||||||
|
|
||||||
func (c *AuthClientConfig) Complete() error {
|
func (c *AuthClientConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
// Resolve tokenSource during configuration loading
|
|
||||||
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
|
||||||
token, err := c.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
// Move the resolved token to the Token field and clear TokenSource
|
|
||||||
c.Token = token
|
|
||||||
c.TokenSource = nil
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +228,10 @@ type AuthOIDCClientConfig struct {
|
|||||||
// Supports http, https, socks5, and socks5h proxy protocols.
|
// Supports http, https, socks5, and socks5h proxy protocols.
|
||||||
// If empty, no proxy is used for OIDC connections.
|
// If empty, no proxy is used for OIDC connections.
|
||||||
ProxyURL string `json:"proxyURL,omitempty"`
|
ProxyURL string `json:"proxyURL,omitempty"`
|
||||||
|
|
||||||
|
// TokenSource specifies a custom dynamic source for the authorization token.
|
||||||
|
// This is mutually exclusive with every other field of this structure.
|
||||||
|
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetConfig struct {
|
type VirtualNetConfig struct {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -38,68 +36,9 @@ func TestClientConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClientConfig_Complete(t *testing.T) {
|
func TestAuthClientConfig_Complete(t *testing.T) {
|
||||||
// Create a temporary file for testing
|
require := require.New(t)
|
||||||
tmpDir := t.TempDir()
|
cfg := &AuthClientConfig{}
|
||||||
testFile := filepath.Join(tmpDir, "test_token")
|
err := cfg.Complete()
|
||||||
testContent := "client-token-value"
|
require.NoError(err)
|
||||||
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
require.EqualValues("token", cfg.Method)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
config AuthClientConfig
|
|
||||||
expectToken string
|
|
||||||
expectPanic bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tokenSource resolved to token",
|
|
||||||
config: AuthClientConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
TokenSource: &ValueSource{
|
|
||||||
Type: "file",
|
|
||||||
File: &FileSource{
|
|
||||||
Path: testFile,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectToken: testContent,
|
|
||||||
expectPanic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "direct token unchanged",
|
|
||||||
config: AuthClientConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
Token: "direct-token",
|
|
||||||
},
|
|
||||||
expectToken: "direct-token",
|
|
||||||
expectPanic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid tokenSource should panic",
|
|
||||||
config: AuthClientConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
TokenSource: &ValueSource{
|
|
||||||
Type: "file",
|
|
||||||
File: &FileSource{
|
|
||||||
Path: "/non/existent/file",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectPanic: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.expectPanic {
|
|
||||||
err := tt.config.Complete()
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
err := tt.config.Complete()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.expectToken, tt.config.Token)
|
|
||||||
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,11 @@ type DomainConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
||||||
|
// This allows individual control over each proxy, complementing the global "start" field.
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
Transport ProxyTransport `json:"transport,omitempty"`
|
Transport ProxyTransport `json:"transport,omitempty"`
|
||||||
// metadata info for each proxy
|
// metadata info for each proxy
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
@@ -138,17 +135,6 @@ type AuthServerConfig struct {
|
|||||||
|
|
||||||
func (c *AuthServerConfig) Complete() error {
|
func (c *AuthServerConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
// Resolve tokenSource during configuration loading
|
|
||||||
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
|
||||||
token, err := c.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
// Move the resolved token to the Token field and clear TokenSource
|
|
||||||
c.Token = token
|
|
||||||
c.TokenSource = nil
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -35,68 +33,9 @@ func TestServerConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthServerConfig_Complete(t *testing.T) {
|
func TestAuthServerConfig_Complete(t *testing.T) {
|
||||||
// Create a temporary file for testing
|
require := require.New(t)
|
||||||
tmpDir := t.TempDir()
|
cfg := &AuthServerConfig{}
|
||||||
testFile := filepath.Join(tmpDir, "test_token")
|
err := cfg.Complete()
|
||||||
testContent := "file-token-value"
|
require.NoError(err)
|
||||||
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
require.EqualValues("token", cfg.Method)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
config AuthServerConfig
|
|
||||||
expectToken string
|
|
||||||
expectPanic bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tokenSource resolved to token",
|
|
||||||
config: AuthServerConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
TokenSource: &ValueSource{
|
|
||||||
Type: "file",
|
|
||||||
File: &FileSource{
|
|
||||||
Path: testFile,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectToken: testContent,
|
|
||||||
expectPanic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "direct token unchanged",
|
|
||||||
config: AuthServerConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
Token: "direct-token",
|
|
||||||
},
|
|
||||||
expectToken: "direct-token",
|
|
||||||
expectPanic: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid tokenSource should panic",
|
|
||||||
config: AuthServerConfig{
|
|
||||||
Method: AuthMethodToken,
|
|
||||||
TokenSource: &ValueSource{
|
|
||||||
Type: "file",
|
|
||||||
File: &FileSource{
|
|
||||||
Path: "/non/existent/file",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectPanic: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.expectPanic {
|
|
||||||
err := tt.config.Complete()
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
err := tt.config.Complete()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.expectToken, tt.config.Token)
|
|
||||||
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,55 +23,109 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/featuregate"
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
)
|
)
|
||||||
// validate feature gates
|
|
||||||
if c.VirtualNet.Address != "" {
|
validators := []func() (Warning, error){
|
||||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
func() (Warning, error) { return validateFeatureGates(c) },
|
||||||
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
||||||
}
|
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||||
|
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||||
|
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||||
|
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
|
for _, validator := range validators {
|
||||||
|
w, err := validator()
|
||||||
|
warnings = AppendError(warnings, w)
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
return warnings, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
||||||
|
if c.VirtualNet.Address != "" {
|
||||||
|
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||||
|
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
|
||||||
|
var errs error
|
||||||
|
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||||
}
|
}
|
||||||
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
|
if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate token/tokenSource mutual exclusivity
|
// Validate token/tokenSource mutual exclusivity
|
||||||
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
|
if c.Token != "" && c.TokenSource != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.Auth.TokenSource != nil {
|
if c.TokenSource != nil {
|
||||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
if c.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateLogConfig(&c.Log); err != nil {
|
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
||||||
errs = AppendError(errs, err)
|
if c.TokenSource == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
var errs error
|
||||||
|
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
|
||||||
|
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
|
||||||
|
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
|
||||||
|
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
||||||
|
}
|
||||||
|
if c.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
|
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
|
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
||||||
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
|
var (
|
||||||
|
warnings Warning
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
|
||||||
|
if c.HeartbeatTimeout < c.HeartbeatInterval {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lo.FromPtr(c.Transport.TLS.Enable) {
|
if !lo.FromPtr(c.TLS.Enable) {
|
||||||
checkTLSConfig := func(name string, value string) Warning {
|
checkTLSConfig := func(name string, value string) Warning {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
||||||
@@ -79,16 +133,20 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
|
if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||||
}
|
}
|
||||||
|
return warnings, errs
|
||||||
|
}
|
||||||
|
|
||||||
for _, f := range c.IncludeConfigFiles {
|
func validateIncludeFiles(files []string) (Warning, error) {
|
||||||
|
var errs error
|
||||||
|
for _, f := range files {
|
||||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
||||||
@@ -98,13 +156,19 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
|||||||
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return warnings, errs
|
return nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
|
func ValidateAllClientConfig(
|
||||||
|
c *v1.ClientCommonConfig,
|
||||||
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
|
) (Warning, error) {
|
||||||
|
validator := NewConfigValidator(unsafeFeatures)
|
||||||
var warnings Warning
|
var warnings Warning
|
||||||
if c != nil {
|
if c != nil {
|
||||||
warning, err := ValidateClientCommonConfig(c)
|
warning, err := validator.ValidateClientCommonConfig(c)
|
||||||
warnings = AppendError(warnings, warning)
|
warnings = AppendError(warnings, warning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return warnings, err
|
return warnings, err
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
@@ -42,6 +43,11 @@ func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
|||||||
|
|
||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.Auth.TokenSource != nil {
|
if c.Auth.TokenSource != nil {
|
||||||
|
if c.Auth.TokenSource.Type == "exec" {
|
||||||
|
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||||
|
errs = AppendError(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
28
pkg/config/v1/validation/validator.go
Normal file
28
pkg/config/v1/validation/validator.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigValidator holds the context dependencies for configuration validation.
|
||||||
|
type ConfigValidator struct {
|
||||||
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigValidator creates a new ConfigValidator instance.
|
||||||
|
func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator {
|
||||||
|
return &ConfigValidator{
|
||||||
|
unsafeFeatures: unsafeFeatures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUnsafeFeature checks if a specific unsafe feature is enabled.
|
||||||
|
func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error {
|
||||||
|
if !v.unsafeFeatures.IsEnabled(feature) {
|
||||||
|
return fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||||
|
"To enable it, ensure it is allowed in the configuration or command line flags", feature)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
type ValueSource struct {
|
type ValueSource struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
File *FileSource `json:"file,omitempty"`
|
File *FileSource `json:"file,omitempty"`
|
||||||
|
Exec *ExecSource `json:"exec,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileSource specifies how to load a value from a file.
|
// FileSource specifies how to load a value from a file.
|
||||||
@@ -34,6 +36,18 @@ type FileSource struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecSource specifies how to get a value from another program launched as subprocess.
|
||||||
|
type ExecSource struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
Env []ExecEnvVar `json:"env,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecEnvVar struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates the ValueSource configuration.
|
// Validate validates the ValueSource configuration.
|
||||||
func (v *ValueSource) Validate() error {
|
func (v *ValueSource) Validate() error {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
@@ -46,8 +60,13 @@ func (v *ValueSource) Validate() error {
|
|||||||
return errors.New("file configuration is required when type is 'file'")
|
return errors.New("file configuration is required when type is 'file'")
|
||||||
}
|
}
|
||||||
return v.File.Validate()
|
return v.File.Validate()
|
||||||
|
case "exec":
|
||||||
|
if v.Exec == nil {
|
||||||
|
return errors.New("exec configuration is required when type is 'exec'")
|
||||||
|
}
|
||||||
|
return v.Exec.Validate()
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
|
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +79,8 @@ func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
|
|||||||
switch v.Type {
|
switch v.Type {
|
||||||
case "file":
|
case "file":
|
||||||
return v.File.Resolve(ctx)
|
return v.File.Resolve(ctx)
|
||||||
|
case "exec":
|
||||||
|
return v.Exec.Resolve(ctx)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
||||||
}
|
}
|
||||||
@@ -91,3 +112,47 @@ func (f *FileSource) Resolve(_ context.Context) (string, error) {
|
|||||||
// Trim whitespace, which is important for file-based tokens
|
// Trim whitespace, which is important for file-based tokens
|
||||||
return strings.TrimSpace(string(content)), nil
|
return strings.TrimSpace(string(content)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates the ExecSource configuration.
|
||||||
|
func (e *ExecSource) Validate() error {
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("execSource cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == "" {
|
||||||
|
return errors.New("exec command cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range e.Env {
|
||||||
|
if env.Name == "" {
|
||||||
|
return errors.New("exec env name cannot be empty")
|
||||||
|
}
|
||||||
|
if strings.Contains(env.Name, "=") {
|
||||||
|
return errors.New("exec env name cannot contain '='")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve reads and returns the content captured from stdout of launched subprocess.
|
||||||
|
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
|
||||||
|
if err := e.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
|
||||||
|
if len(e.Env) != 0 {
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
for _, env := range e.Env {
|
||||||
|
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace, which is important for exec-based tokens
|
||||||
|
return strings.TrimSpace(string(content)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ type VisitorTransport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VisitorBaseConfig struct {
|
type VisitorBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
||||||
|
// This allows individual control over each visitor, complementing the global "start" field.
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Transport VisitorTransport `json:"transport,omitempty"`
|
Transport VisitorTransport `json:"transport,omitempty"`
|
||||||
SecretKey string `json:"secretKey,omitempty"`
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
// if the server user is not set, it defaults to the current user
|
// if the server user is not set, it defaults to the current user
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ func (d *Dispatcher) Send(m Message) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dispatcher) SendChannel() chan Message {
|
|
||||||
return d.sendCh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
||||||
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type Login struct {
|
|||||||
PrivilegeKey string `json:"privilege_key,omitempty"`
|
PrivilegeKey string `json:"privilege_key,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
RunID string `json:"run_id,omitempty"`
|
RunID string `json:"run_id,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
Metas map[string]string `json:"metas,omitempty"`
|
Metas map[string]string `json:"metas,omitempty"`
|
||||||
|
|
||||||
// Currently only effective for VirtualClient.
|
// Currently only effective for VirtualClient.
|
||||||
|
|||||||
@@ -23,11 +23,20 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PluginContext provides the necessary context and callbacks for visitor plugins.
|
||||||
type PluginContext struct {
|
type PluginContext struct {
|
||||||
Name string
|
// Name is the unique identifier for this visitor, used for logging and routing.
|
||||||
Ctx context.Context
|
Name string
|
||||||
|
|
||||||
|
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
|
||||||
|
Ctx context.Context
|
||||||
|
|
||||||
|
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
|
||||||
VnetController *vnet.Controller
|
VnetController *vnet.Controller
|
||||||
HandleConn func(net.Conn)
|
|
||||||
|
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
|
||||||
|
// Does not return error; failures are handled by closing the connection.
|
||||||
|
SendConnToVisitor func(net.Conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creators is used for create plugins to handle connections.
|
// Creators is used for create plugins to handle connections.
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ type VirtualNetPlugin struct {
|
|||||||
controllerConn net.Conn
|
controllerConn net.Conn
|
||||||
closeSignal chan struct{}
|
closeSignal chan struct{}
|
||||||
|
|
||||||
|
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,6 @@ func (p *VirtualNetPlugin) Start() {
|
|||||||
|
|
||||||
func (p *VirtualNetPlugin) run() {
|
func (p *VirtualNetPlugin) run() {
|
||||||
xl := xlog.FromContextSafe(p.ctx)
|
xl := xlog.FromContextSafe(p.ctx)
|
||||||
reconnectDelay := 10 * time.Second
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
currentCloseSignal := make(chan struct{})
|
currentCloseSignal := make(chan struct{})
|
||||||
@@ -121,7 +122,10 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.controllerConn = controllerConn
|
p.controllerConn = controllerConn
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
|
// Wrap with CloseNotifyConn which supports both close notification and error recording
|
||||||
|
var closeErr error
|
||||||
|
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
|
||||||
|
closeErr = err
|
||||||
close(currentCloseSignal) // Signal the run loop on close.
|
close(currentCloseSignal) // Signal the run loop on close.
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -129,9 +133,9 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
||||||
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
||||||
|
|
||||||
// Pass the CloseNotifyConn to HandleConn.
|
// Pass the CloseNotifyConn to the visitor for handling.
|
||||||
// HandleConn is responsible for calling Close() on pluginNotifyConn.
|
// The visitor can call CloseWithError to record the failure reason.
|
||||||
p.pluginCtx.HandleConn(pluginNotifyConn)
|
p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
|
||||||
|
|
||||||
// Wait for context cancellation or connection close.
|
// Wait for context cancellation or connection close.
|
||||||
select {
|
select {
|
||||||
@@ -140,8 +144,32 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
return
|
return
|
||||||
case <-currentCloseSignal:
|
case <-currentCloseSignal:
|
||||||
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
|
// Determine reconnect delay based on error with exponential backoff
|
||||||
// HandleConn closed the plugin side. Close the controller side.
|
var reconnectDelay time.Duration
|
||||||
|
if closeErr != nil {
|
||||||
|
p.consecutiveErrors++
|
||||||
|
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
|
||||||
|
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
|
||||||
|
|
||||||
|
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||||
|
baseDelay := 60 * time.Second
|
||||||
|
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
||||||
|
if reconnectDelay > 300*time.Second {
|
||||||
|
reconnectDelay = 300 * time.Second
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset consecutive errors on successful connection
|
||||||
|
if p.consecutiveErrors > 0 {
|
||||||
|
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
|
||||||
|
p.pluginCtx.Name, p.consecutiveErrors)
|
||||||
|
p.consecutiveErrors = 0
|
||||||
|
} else {
|
||||||
|
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
|
||||||
|
}
|
||||||
|
reconnectDelay = 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// The visitor closed the plugin side. Close the controller side.
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
|
|
||||||
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
||||||
@@ -184,7 +212,7 @@ func (p *VirtualNetPlugin) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly close the controller side of the pipe.
|
// Explicitly close the controller side of the pipe.
|
||||||
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
|
// This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
||||||
|
|
||||||
|
|||||||
34
pkg/policy/security/unsafe.go
Normal file
34
pkg/policy/security/unsafe.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenSourceExec = "TokenSourceExec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ClientUnsafeFeatures = []string{
|
||||||
|
TokenSourceExec,
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerUnsafeFeatures = []string{
|
||||||
|
TokenSourceExec,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnsafeFeatures struct {
|
||||||
|
features map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnsafeFeatures(allowed []string) *UnsafeFeatures {
|
||||||
|
features := make(map[string]bool)
|
||||||
|
for _, f := range allowed {
|
||||||
|
features[f] = true
|
||||||
|
}
|
||||||
|
return &UnsafeFeatures{features: features}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnsafeFeatures) IsEnabled(feature string) bool {
|
||||||
|
if u == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return u.features[feature]
|
||||||
|
}
|
||||||
@@ -35,15 +35,19 @@ type MessageTransporter interface {
|
|||||||
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter {
|
type MessageSender interface {
|
||||||
|
Send(msg.Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageTransporter(sender MessageSender) MessageTransporter {
|
||||||
return &transporterImpl{
|
return &transporterImpl{
|
||||||
sendCh: sendCh,
|
sender: sender,
|
||||||
registry: make(map[string]map[string]chan msg.Message),
|
registry: make(map[string]map[string]chan msg.Message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type transporterImpl struct {
|
type transporterImpl struct {
|
||||||
sendCh chan msg.Message
|
sender MessageSender
|
||||||
|
|
||||||
// First key is message type and second key is lane key.
|
// First key is message type and second key is lane key.
|
||||||
// Dispatch will dispatch message to related channel by its message type
|
// Dispatch will dispatch message to related channel by its message type
|
||||||
@@ -53,9 +57,7 @@ type transporterImpl struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (impl *transporterImpl) Send(m msg.Message) error {
|
func (impl *transporterImpl) Send(m msg.Message) error {
|
||||||
return errors.PanicToError(func() {
|
return impl.sender.Send(m)
|
||||||
impl.sendCh <- m
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
|
|||||||
// 1 means closed
|
// 1 means closed
|
||||||
closeFlag int32
|
closeFlag int32
|
||||||
|
|
||||||
closeFn func()
|
closeFn func(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeFn will be only called once
|
// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
|
||||||
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
||||||
return &CloseNotifyConn{
|
return &CloseNotifyConn{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
closeFn: closeFn,
|
closeFn: closeFn,
|
||||||
@@ -149,14 +149,27 @@ func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
|||||||
func (cc *CloseNotifyConn) Close() (err error) {
|
func (cc *CloseNotifyConn) Close() (err error) {
|
||||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||||
if pflag == 0 {
|
if pflag == 0 {
|
||||||
err = cc.Close()
|
err = cc.Conn.Close()
|
||||||
if cc.closeFn != nil {
|
if cc.closeFn != nil {
|
||||||
cc.closeFn()
|
cc.closeFn(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseWithError closes the connection and passes the error to the close callback.
|
||||||
|
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
||||||
|
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
||||||
|
if pflag == 0 {
|
||||||
|
closeErr := cc.Conn.Close()
|
||||||
|
if cc.closeFn != nil {
|
||||||
|
cc.closeFn(err)
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type StatsConn struct {
|
type StatsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
|||||||
muxer := http.NewServeMux()
|
muxer := http.NewServeMux()
|
||||||
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
||||||
notifyCh := make(chan struct{})
|
notifyCh := make(chan struct{})
|
||||||
conn := WrapCloseNotifyConn(c, func() {
|
conn := WrapCloseNotifyConn(c, func(_ error) {
|
||||||
close(notifyCh)
|
close(notifyCh)
|
||||||
})
|
})
|
||||||
wl.acceptCh <- conn
|
wl.acceptCh <- conn
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "0.65.0"
|
var version = "0.66.0"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
146
server/client_registry.go
Normal file
146
server/client_registry.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientInfo captures metadata about a connected frpc instance.
|
||||||
|
type ClientInfo struct {
|
||||||
|
Key string
|
||||||
|
User string
|
||||||
|
ClientID string
|
||||||
|
RunID string
|
||||||
|
Hostname string
|
||||||
|
Metas map[string]string
|
||||||
|
FirstConnectedAt time.Time
|
||||||
|
LastConnectedAt time.Time
|
||||||
|
DisconnectedAt time.Time
|
||||||
|
Online bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientRegistry keeps track of active clients keyed by "{user}.{clientID}" (or runID if clientID is empty).
|
||||||
|
// Entries without an explicit clientID are removed on disconnect to avoid stale offline records.
|
||||||
|
type ClientRegistry struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
clients map[string]*ClientInfo
|
||||||
|
runIndex map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientRegistry() *ClientRegistry {
|
||||||
|
return &ClientRegistry{
|
||||||
|
clients: make(map[string]*ClientInfo),
|
||||||
|
runIndex: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
|
||||||
|
func (cr *ClientRegistry) Register(user, clientID, runID, hostname string, metas map[string]string) (key string, conflict bool) {
|
||||||
|
if runID == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveID := clientID
|
||||||
|
if effectiveID == "" {
|
||||||
|
effectiveID = runID
|
||||||
|
}
|
||||||
|
key = cr.composeClientKey(user, effectiveID)
|
||||||
|
enforceUnique := clientID != ""
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
cr.mu.Lock()
|
||||||
|
defer cr.mu.Unlock()
|
||||||
|
|
||||||
|
info, exists := cr.clients[key]
|
||||||
|
if enforceUnique && exists && info.Online && info.RunID != "" && info.RunID != runID {
|
||||||
|
return key, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
info = &ClientInfo{
|
||||||
|
Key: key,
|
||||||
|
User: user,
|
||||||
|
ClientID: clientID,
|
||||||
|
FirstConnectedAt: now,
|
||||||
|
}
|
||||||
|
cr.clients[key] = info
|
||||||
|
} else if info.RunID != "" {
|
||||||
|
delete(cr.runIndex, info.RunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
info.RunID = runID
|
||||||
|
info.Hostname = hostname
|
||||||
|
info.Metas = metas
|
||||||
|
if info.FirstConnectedAt.IsZero() {
|
||||||
|
info.FirstConnectedAt = now
|
||||||
|
}
|
||||||
|
info.LastConnectedAt = now
|
||||||
|
info.DisconnectedAt = time.Time{}
|
||||||
|
info.Online = true
|
||||||
|
|
||||||
|
cr.runIndex[runID] = key
|
||||||
|
return key, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkOfflineByRunID marks the client as offline when the corresponding control disconnects.
|
||||||
|
func (cr *ClientRegistry) MarkOfflineByRunID(runID string) {
|
||||||
|
cr.mu.Lock()
|
||||||
|
defer cr.mu.Unlock()
|
||||||
|
|
||||||
|
key, ok := cr.runIndex[runID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info, ok := cr.clients[key]; ok && info.RunID == runID {
|
||||||
|
if info.ClientID == "" {
|
||||||
|
delete(cr.clients, key)
|
||||||
|
} else {
|
||||||
|
info.RunID = ""
|
||||||
|
info.Online = false
|
||||||
|
now := time.Now()
|
||||||
|
info.DisconnectedAt = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(cr.runIndex, runID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a snapshot of all known clients.
|
||||||
|
func (cr *ClientRegistry) List() []ClientInfo {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]ClientInfo, 0, len(cr.clients))
|
||||||
|
for _, info := range cr.clients {
|
||||||
|
cp := *info
|
||||||
|
cp.Metas = maps.Clone(info.Metas)
|
||||||
|
result = append(result, cp)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByKey retrieves a client by its composite key ({user}.{clientID} or runID fallback).
|
||||||
|
func (cr *ClientRegistry) GetByKey(key string) (ClientInfo, bool) {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
info, ok := cr.clients[key]
|
||||||
|
if !ok {
|
||||||
|
return ClientInfo{}, false
|
||||||
|
}
|
||||||
|
cp := *info
|
||||||
|
cp.Metas = maps.Clone(info.Metas)
|
||||||
|
return cp, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ClientRegistry) composeClientKey(user, id string) string {
|
||||||
|
switch {
|
||||||
|
case user == "":
|
||||||
|
return id
|
||||||
|
case id == "":
|
||||||
|
return user
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s.%s", user, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,8 @@ type Control struct {
|
|||||||
|
|
||||||
// verifies authentication based on selected method
|
// verifies authentication based on selected method
|
||||||
authVerifier auth.Verifier
|
authVerifier auth.Verifier
|
||||||
|
// key used for connection encryption
|
||||||
|
encryptionKey []byte
|
||||||
|
|
||||||
// other components can use this to communicate with client
|
// other components can use this to communicate with client
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
@@ -145,6 +147,8 @@ type Control struct {
|
|||||||
// Server configuration information
|
// Server configuration information
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
|
|
||||||
|
clientRegistry *ClientRegistry
|
||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
@@ -157,6 +161,7 @@ func NewControl(
|
|||||||
pxyManager *proxy.Manager,
|
pxyManager *proxy.Manager,
|
||||||
pluginManager *plugin.Manager,
|
pluginManager *plugin.Manager,
|
||||||
authVerifier auth.Verifier,
|
authVerifier auth.Verifier,
|
||||||
|
encryptionKey []byte,
|
||||||
ctlConn net.Conn,
|
ctlConn net.Conn,
|
||||||
ctlConnEncrypted bool,
|
ctlConnEncrypted bool,
|
||||||
loginMsg *msg.Login,
|
loginMsg *msg.Login,
|
||||||
@@ -171,6 +176,7 @@ func NewControl(
|
|||||||
pxyManager: pxyManager,
|
pxyManager: pxyManager,
|
||||||
pluginManager: pluginManager,
|
pluginManager: pluginManager,
|
||||||
authVerifier: authVerifier,
|
authVerifier: authVerifier,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
conn: ctlConn,
|
conn: ctlConn,
|
||||||
loginMsg: loginMsg,
|
loginMsg: loginMsg,
|
||||||
workConnCh: make(chan net.Conn, poolCount+10),
|
workConnCh: make(chan net.Conn, poolCount+10),
|
||||||
@@ -186,7 +192,7 @@ func NewControl(
|
|||||||
ctl.lastPing.Store(time.Now())
|
ctl.lastPing.Store(time.Now())
|
||||||
|
|
||||||
if ctlConnEncrypted {
|
if ctlConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
|
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -195,7 +201,7 @@ func NewControl(
|
|||||||
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +360,7 @@ func (ctl *Control) worker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics.Server.CloseClient()
|
metrics.Server.CloseClient()
|
||||||
|
ctl.clientRegistry.MarkOfflineByRunID(ctl.runID)
|
||||||
xl.Infof("client exit success")
|
xl.Infof("client exit success")
|
||||||
close(ctl.doneCh)
|
close(ctl.doneCh)
|
||||||
}
|
}
|
||||||
@@ -478,6 +485,7 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
GetWorkConnFn: ctl.GetWorkConn,
|
GetWorkConnFn: ctl.GetWorkConn,
|
||||||
Configurer: pxyConf,
|
Configurer: pxyConf,
|
||||||
ServerCfg: ctl.serverCfg,
|
ServerCfg: ctl.serverCfg,
|
||||||
|
EncryptionKey: ctl.encryptionKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return remoteAddr, err
|
return remoteAddr, err
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ type ResourceController struct {
|
|||||||
// HTTP Group Controller
|
// HTTP Group Controller
|
||||||
HTTPGroupCtl *group.HTTPGroupController
|
HTTPGroupCtl *group.HTTPGroupController
|
||||||
|
|
||||||
|
// HTTPS Group Controller
|
||||||
|
HTTPSGroupCtl *group.HTTPSGroupController
|
||||||
|
|
||||||
// TCP Mux Group Controller
|
// TCP Mux Group Controller
|
||||||
TCPMuxGroupCtl *group.TCPMuxGroupCtl
|
TCPMuxGroupCtl *group.TCPMuxGroupCtl
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ package server
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
@@ -53,6 +56,8 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
|
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
|
||||||
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
|
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
|
||||||
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients", svr.apiClientList).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/clients/{key}", svr.apiClientDetail).Methods("GET")
|
||||||
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
|
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
|
||||||
|
|
||||||
// view
|
// view
|
||||||
@@ -88,6 +93,19 @@ type serverInfoResp struct {
|
|||||||
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type clientInfoResp struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
User string `json:"user"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
RunID string `json:"runId"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Metas map[string]string `json:"metas,omitempty"`
|
||||||
|
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
||||||
|
LastConnectedAt int64 `json:"lastConnectedAt"`
|
||||||
|
DisconnectedAt int64 `json:"disconnectedAt,omitempty"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
}
|
||||||
|
|
||||||
// /healthz
|
// /healthz
|
||||||
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
@@ -132,6 +150,101 @@ func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
res.Msg = string(buf)
|
res.Msg = string(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /api/clients
|
||||||
|
func (svr *Service) apiClientList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.RequestURI(), res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.RequestURI())
|
||||||
|
|
||||||
|
if svr.clientRegistry == nil {
|
||||||
|
res.Code = http.StatusInternalServerError
|
||||||
|
res.Msg = "client registry unavailable"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
userFilter := query.Get("user")
|
||||||
|
clientIDFilter := query.Get("clientId")
|
||||||
|
runIDFilter := query.Get("runId")
|
||||||
|
statusFilter := strings.ToLower(query.Get("status"))
|
||||||
|
|
||||||
|
records := svr.clientRegistry.List()
|
||||||
|
items := make([]clientInfoResp, 0, len(records))
|
||||||
|
for _, info := range records {
|
||||||
|
if userFilter != "" && info.User != userFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if clientIDFilter != "" && info.ClientID != clientIDFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if runIDFilter != "" && info.RunID != runIDFilter {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchStatusFilter(info.Online, statusFilter) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, buildClientInfoResp(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(items, func(a, b clientInfoResp) int {
|
||||||
|
if v := cmp.Compare(a.User, b.User); v != 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.Key, b.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(items)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/clients/{key}
|
||||||
|
func (svr *Service) apiClientDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.RequestURI(), res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.RequestURI())
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
key := vars["key"]
|
||||||
|
if key == "" {
|
||||||
|
res.Code = http.StatusBadRequest
|
||||||
|
res.Msg = "missing client key"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if svr.clientRegistry == nil {
|
||||||
|
res.Code = http.StatusInternalServerError
|
||||||
|
res.Msg = "client registry unavailable"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, ok := svr.clientRegistry.GetByKey(key)
|
||||||
|
if !ok {
|
||||||
|
res.Code = http.StatusNotFound
|
||||||
|
res.Msg = fmt.Sprintf("client %s not found", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(buildClientInfoResp(info))
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
type BaseOutConf struct {
|
type BaseOutConf struct {
|
||||||
v1.ProxyBaseConfig
|
v1.ProxyBaseConfig
|
||||||
}
|
}
|
||||||
@@ -404,3 +517,41 @@ func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
|
|||||||
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
||||||
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildClientInfoResp(info ClientInfo) clientInfoResp {
|
||||||
|
resp := clientInfoResp{
|
||||||
|
Key: info.Key,
|
||||||
|
User: info.User,
|
||||||
|
ClientID: info.ClientID,
|
||||||
|
RunID: info.RunID,
|
||||||
|
Hostname: info.Hostname,
|
||||||
|
Metas: info.Metas,
|
||||||
|
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
||||||
|
LastConnectedAt: toUnix(info.LastConnectedAt),
|
||||||
|
Online: info.Online,
|
||||||
|
}
|
||||||
|
if !info.DisconnectedAt.IsZero() {
|
||||||
|
resp.DisconnectedAt = info.DisconnectedAt.Unix()
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUnix(t time.Time) int64 {
|
||||||
|
if t.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return t.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStatusFilter(online bool, filter string) bool {
|
||||||
|
switch strings.ToLower(filter) {
|
||||||
|
case "", "all":
|
||||||
|
return true
|
||||||
|
case "online":
|
||||||
|
return online
|
||||||
|
case "offline":
|
||||||
|
return !online
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
197
server/group/https.go
Normal file
197
server/group/https.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// Copyright 2025 The frp Authors
|
||||||
|
//
|
||||||
|
// 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 group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
gerr "github.com/fatedier/golib/errors"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPSGroupController struct {
|
||||||
|
groups map[string]*HTTPSGroup
|
||||||
|
|
||||||
|
httpsMuxer *vhost.HTTPSMuxer
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSGroupController(httpsMuxer *vhost.HTTPSMuxer) *HTTPSGroupController {
|
||||||
|
return &HTTPSGroupController{
|
||||||
|
groups: make(map[string]*HTTPSGroup),
|
||||||
|
httpsMuxer: httpsMuxer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *HTTPSGroupController) Listen(
|
||||||
|
ctx context.Context,
|
||||||
|
group, groupKey string,
|
||||||
|
routeConfig vhost.RouteConfig,
|
||||||
|
) (l net.Listener, err error) {
|
||||||
|
indexKey := group
|
||||||
|
ctl.mu.Lock()
|
||||||
|
g, ok := ctl.groups[indexKey]
|
||||||
|
if !ok {
|
||||||
|
g = NewHTTPSGroup(ctl)
|
||||||
|
ctl.groups[indexKey] = g
|
||||||
|
}
|
||||||
|
ctl.mu.Unlock()
|
||||||
|
|
||||||
|
return g.Listen(ctx, group, groupKey, routeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *HTTPSGroupController) RemoveGroup(group string) {
|
||||||
|
ctl.mu.Lock()
|
||||||
|
defer ctl.mu.Unlock()
|
||||||
|
delete(ctl.groups, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSGroup struct {
|
||||||
|
group string
|
||||||
|
groupKey string
|
||||||
|
domain string
|
||||||
|
|
||||||
|
acceptCh chan net.Conn
|
||||||
|
httpsLn *vhost.Listener
|
||||||
|
lns []*HTTPSGroupListener
|
||||||
|
ctl *HTTPSGroupController
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
|
||||||
|
return &HTTPSGroup{
|
||||||
|
lns: make([]*HTTPSGroupListener, 0),
|
||||||
|
ctl: ctl,
|
||||||
|
acceptCh: make(chan net.Conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) Listen(
|
||||||
|
ctx context.Context,
|
||||||
|
group, groupKey string,
|
||||||
|
routeConfig vhost.RouteConfig,
|
||||||
|
) (ln *HTTPSGroupListener, err error) {
|
||||||
|
g.mu.Lock()
|
||||||
|
defer g.mu.Unlock()
|
||||||
|
if len(g.lns) == 0 {
|
||||||
|
// the first listener, listen on the real address
|
||||||
|
httpsLn, errRet := g.ctl.httpsMuxer.Listen(ctx, &routeConfig)
|
||||||
|
if errRet != nil {
|
||||||
|
return nil, errRet
|
||||||
|
}
|
||||||
|
ln = newHTTPSGroupListener(group, g, httpsLn.Addr())
|
||||||
|
|
||||||
|
g.group = group
|
||||||
|
g.groupKey = groupKey
|
||||||
|
g.domain = routeConfig.Domain
|
||||||
|
g.httpsLn = httpsLn
|
||||||
|
g.lns = append(g.lns, ln)
|
||||||
|
go g.worker()
|
||||||
|
} else {
|
||||||
|
// route config in the same group must be equal
|
||||||
|
if g.group != group || g.domain != routeConfig.Domain {
|
||||||
|
return nil, ErrGroupParamsInvalid
|
||||||
|
}
|
||||||
|
if g.groupKey != groupKey {
|
||||||
|
return nil, ErrGroupAuthFailed
|
||||||
|
}
|
||||||
|
ln = newHTTPSGroupListener(group, g, g.lns[0].Addr())
|
||||||
|
g.lns = append(g.lns, ln)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) worker() {
|
||||||
|
for {
|
||||||
|
c, err := g.httpsLn.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = gerr.PanicToError(func() {
|
||||||
|
g.acceptCh <- c
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) Accept() <-chan net.Conn {
|
||||||
|
return g.acceptCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) CloseListener(ln *HTTPSGroupListener) {
|
||||||
|
g.mu.Lock()
|
||||||
|
defer g.mu.Unlock()
|
||||||
|
for i, tmpLn := range g.lns {
|
||||||
|
if tmpLn == ln {
|
||||||
|
g.lns = append(g.lns[:i], g.lns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(g.lns) == 0 {
|
||||||
|
close(g.acceptCh)
|
||||||
|
if g.httpsLn != nil {
|
||||||
|
g.httpsLn.Close()
|
||||||
|
}
|
||||||
|
g.ctl.RemoveGroup(g.group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSGroupListener struct {
|
||||||
|
groupName string
|
||||||
|
group *HTTPSGroup
|
||||||
|
|
||||||
|
addr net.Addr
|
||||||
|
closeCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPSGroupListener(name string, group *HTTPSGroup, addr net.Addr) *HTTPSGroupListener {
|
||||||
|
return &HTTPSGroupListener{
|
||||||
|
groupName: name,
|
||||||
|
group: group,
|
||||||
|
addr: addr,
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *HTTPSGroupListener) Accept() (c net.Conn, err error) {
|
||||||
|
var ok bool
|
||||||
|
select {
|
||||||
|
case <-ln.closeCh:
|
||||||
|
return nil, ErrListenerClosed
|
||||||
|
case c, ok = <-ln.group.Accept():
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrListenerClosed
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *HTTPSGroupListener) Addr() net.Addr {
|
||||||
|
return ln.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ln *HTTPSGroupListener) Close() (err error) {
|
||||||
|
close(ln.closeCh)
|
||||||
|
|
||||||
|
// remove self from HTTPSGroup
|
||||||
|
ln.group.CloseListener(ln)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -165,7 +165,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = tmpConn
|
var rwc io.ReadWriteCloser = tmpConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -58,27 +59,24 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
routeConfig.Domain = domain
|
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
if err != nil {
|
||||||
if errRet != nil {
|
return "", err
|
||||||
err = errRet
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
|
||||||
pxy.listeners = append(pxy.listeners, l)
|
pxy.listeners = append(pxy.listeners, l)
|
||||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort))
|
||||||
|
xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pxy.cfg.SubDomain != "" {
|
if pxy.cfg.SubDomain != "" {
|
||||||
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
domain := pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
l, err := pxy.listenForDomain(routeConfig, domain)
|
||||||
if errRet != nil {
|
if err != nil {
|
||||||
err = errRet
|
return "", err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
|
||||||
pxy.listeners = append(pxy.listeners, l)
|
pxy.listeners = append(pxy.listeners, l)
|
||||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
addrs = append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.VhostHTTPSPort))
|
||||||
|
xl.Infof("https proxy listen for host [%s] group [%s]", domain, pxy.cfg.LoadBalancer.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
pxy.startCommonTCPListenersHandler()
|
pxy.startCommonTCPListenersHandler()
|
||||||
@@ -89,3 +87,18 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
|||||||
func (pxy *HTTPSProxy) Close() {
|
func (pxy *HTTPSProxy) Close() {
|
||||||
pxy.BaseProxy.Close()
|
pxy.BaseProxy.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pxy *HTTPSProxy) listenForDomain(routeConfig *vhost.RouteConfig, domain string) (net.Listener, error) {
|
||||||
|
tmpRouteConfig := *routeConfig
|
||||||
|
tmpRouteConfig.Domain = domain
|
||||||
|
|
||||||
|
if pxy.cfg.LoadBalancer.Group != "" {
|
||||||
|
return pxy.rc.HTTPSGroupCtl.Listen(
|
||||||
|
pxy.ctx,
|
||||||
|
pxy.cfg.LoadBalancer.Group,
|
||||||
|
pxy.cfg.LoadBalancer.GroupKey,
|
||||||
|
tmpRouteConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &tmpRouteConfig)
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type BaseProxy struct {
|
|||||||
poolCount int
|
poolCount int
|
||||||
getWorkConnFn GetWorkConnFn
|
getWorkConnFn GetWorkConnFn
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
|
encryptionKey []byte
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
userInfo plugin.UserInfo
|
userInfo plugin.UserInfo
|
||||||
loginMsg *msg.Login
|
loginMsg *msg.Login
|
||||||
@@ -213,7 +214,6 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl := xlog.FromContextSafe(pxy.Context())
|
xl := xlog.FromContextSafe(pxy.Context())
|
||||||
defer userConn.Close()
|
defer userConn.Close()
|
||||||
|
|
||||||
serverCfg := pxy.serverCfg
|
|
||||||
cfg := pxy.configurer.GetBaseConfig()
|
cfg := pxy.configurer.GetBaseConfig()
|
||||||
// server plugin hook
|
// server plugin hook
|
||||||
rc := pxy.GetResourceController()
|
rc := pxy.GetResourceController()
|
||||||
@@ -240,7 +240,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
||||||
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
||||||
if cfg.Transport.UseEncryption {
|
if cfg.Transport.UseEncryption {
|
||||||
local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token))
|
local, err = libio.WithEncryption(local, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
@@ -279,6 +279,7 @@ type Options struct {
|
|||||||
GetWorkConnFn GetWorkConnFn
|
GetWorkConnFn GetWorkConnFn
|
||||||
Configurer v1.ProxyConfigurer
|
Configurer v1.ProxyConfigurer
|
||||||
ServerCfg *v1.ServerConfig
|
ServerCfg *v1.ServerConfig
|
||||||
|
EncryptionKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
||||||
@@ -298,6 +299,7 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
|||||||
poolCount: options.PoolCount,
|
poolCount: options.PoolCount,
|
||||||
getWorkConnFn: options.GetWorkConnFn,
|
getWorkConnFn: options.GetWorkConnFn,
|
||||||
serverCfg: options.ServerCfg,
|
serverCfg: options.ServerCfg,
|
||||||
|
encryptionKey: options.EncryptionKey,
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = workConn
|
var rwc io.ReadWriteCloser = workConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ type Service struct {
|
|||||||
// Manage all controllers
|
// Manage all controllers
|
||||||
ctlManager *ControlManager
|
ctlManager *ControlManager
|
||||||
|
|
||||||
|
// Track logical clients keyed by user.clientID.
|
||||||
|
clientRegistry *ClientRegistry
|
||||||
|
|
||||||
// Manage all proxies
|
// Manage all proxies
|
||||||
pxyManager *proxy.Manager
|
pxyManager *proxy.Manager
|
||||||
|
|
||||||
@@ -113,8 +116,8 @@ type Service struct {
|
|||||||
|
|
||||||
sshTunnelGateway *ssh.Gateway
|
sshTunnelGateway *ssh.Gateway
|
||||||
|
|
||||||
// Verifies authentication based on selected method
|
// Auth runtime and encryption materials
|
||||||
authVerifier auth.Verifier
|
auth *auth.ServerAuth
|
||||||
|
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
@@ -149,10 +152,16 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authRuntime, err := auth.BuildServerAuth(&cfg.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
svr := &Service{
|
svr := &Service{
|
||||||
ctlManager: NewControlManager(),
|
ctlManager: NewControlManager(),
|
||||||
pxyManager: proxy.NewManager(),
|
clientRegistry: NewClientRegistry(),
|
||||||
pluginManager: plugin.NewManager(),
|
pxyManager: proxy.NewManager(),
|
||||||
|
pluginManager: plugin.NewManager(),
|
||||||
rc: &controller.ResourceController{
|
rc: &controller.ResourceController{
|
||||||
VisitorManager: visitor.NewManager(),
|
VisitorManager: visitor.NewManager(),
|
||||||
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
||||||
@@ -160,7 +169,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
},
|
},
|
||||||
sshTunnelListener: netpkg.NewInternalListener(),
|
sshTunnelListener: netpkg.NewInternalListener(),
|
||||||
httpVhostRouter: vhost.NewRouters(),
|
httpVhostRouter: vhost.NewRouters(),
|
||||||
authVerifier: auth.NewAuthVerifier(cfg.Auth),
|
auth: authRuntime,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -322,6 +331,9 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
|
return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init HTTPS group controller after HTTPSMuxer is created
|
||||||
|
svr.rc.HTTPSGroupCtl = group.NewHTTPSGroupController(svr.rc.VhostHTTPSMuxer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// frp tls listener
|
// frp tls listener
|
||||||
@@ -583,7 +595,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
||||||
|
|
||||||
// Check auth.
|
// Check auth.
|
||||||
authVerifier := svr.authVerifier
|
authVerifier := svr.auth.Verifier
|
||||||
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
||||||
authVerifier = auth.AlwaysPassVerifier
|
authVerifier = auth.AlwaysPassVerifier
|
||||||
}
|
}
|
||||||
@@ -592,16 +604,25 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): use SessionContext
|
// TODO(fatedier): use SessionContext
|
||||||
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg)
|
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("create new controller error: %v", err)
|
xl.Warnf("create new controller error: %v", err)
|
||||||
// don't return detailed errors to client
|
// don't return detailed errors to client
|
||||||
return fmt.Errorf("unexpected error when creating new controller")
|
return fmt.Errorf("unexpected error when creating new controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
||||||
oldCtl.WaitClosed()
|
oldCtl.WaitClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, loginMsg.Metas)
|
||||||
|
if conflict {
|
||||||
|
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
||||||
|
ctl.Close()
|
||||||
|
return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
|
||||||
|
}
|
||||||
|
ctl.clientRegistry = svr.clientRegistry
|
||||||
|
|
||||||
ctl.Start()
|
ctl.Start()
|
||||||
|
|
||||||
// for statistics
|
// for statistics
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return p, p.StdOutput(), err
|
return p, p.StdOutput(), err
|
||||||
}
|
}
|
||||||
// sleep for a while to get std output
|
// Give frps extra time to finish binding ports before proceeding.
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(4 * time.Second)
|
||||||
return p, p.StdOutput(), nil
|
return p, p.StdOutput(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ import (
|
|||||||
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
||||||
f := framework.NewDefaultFramework()
|
f := framework.NewDefaultFramework()
|
||||||
|
|
||||||
|
createExecTokenScript := func(name string) string {
|
||||||
|
scriptPath := filepath.Join(f.TempDirectory, name)
|
||||||
|
scriptContent := `#!/bin/sh
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
`
|
||||||
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0o600)
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
err = os.Chmod(scriptPath, 0o700)
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
return scriptPath
|
||||||
|
}
|
||||||
|
|
||||||
ginkgo.Describe("File-based token loading", func() {
|
ginkgo.Describe("File-based token loading", func() {
|
||||||
ginkgo.It("should work with file tokenSource", func() {
|
ginkgo.It("should work with file tokenSource", func() {
|
||||||
// Create a temporary token file
|
// Create a temporary token file
|
||||||
@@ -214,4 +226,154 @@ auth.tokenSource.file.path = "%s"
|
|||||||
f.RunProcesses([]string{serverConf}, []string{})
|
f.RunProcesses([]string{serverConf}, []string{})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.Describe("Exec-based token loading", func() {
|
||||||
|
ginkgo.It("should work with server tokenSource", func() {
|
||||||
|
execValue := "exec-server-value"
|
||||||
|
scriptPath := createExecTokenScript("server_token_exec.sh")
|
||||||
|
|
||||||
|
serverPort := f.AllocPort()
|
||||||
|
remotePort := f.AllocPort()
|
||||||
|
|
||||||
|
serverConf := fmt.Sprintf(`
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = %d
|
||||||
|
|
||||||
|
auth.tokenSource.type = "exec"
|
||||||
|
auth.tokenSource.exec.command = %q
|
||||||
|
auth.tokenSource.exec.args = [%q]
|
||||||
|
`, serverPort, scriptPath, execValue)
|
||||||
|
|
||||||
|
clientConf := fmt.Sprintf(`
|
||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = %d
|
||||||
|
loginFailExit = false
|
||||||
|
auth.token = %q
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "tcp"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = %d
|
||||||
|
remotePort = %d
|
||||||
|
`, serverPort, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
||||||
|
|
||||||
|
serverConfigPath := f.GenerateConfigFile(serverConf)
|
||||||
|
clientConfigPath := f.GenerateConfigFile(clientConf)
|
||||||
|
|
||||||
|
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should work with client tokenSource", func() {
|
||||||
|
execValue := "exec-client-value"
|
||||||
|
scriptPath := createExecTokenScript("client_token_exec.sh")
|
||||||
|
|
||||||
|
serverPort := f.AllocPort()
|
||||||
|
remotePort := f.AllocPort()
|
||||||
|
|
||||||
|
serverConf := fmt.Sprintf(`
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = %d
|
||||||
|
|
||||||
|
auth.token = %q
|
||||||
|
`, serverPort, execValue)
|
||||||
|
|
||||||
|
clientConf := fmt.Sprintf(`
|
||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = %d
|
||||||
|
loginFailExit = false
|
||||||
|
|
||||||
|
auth.tokenSource.type = "exec"
|
||||||
|
auth.tokenSource.exec.command = %q
|
||||||
|
auth.tokenSource.exec.args = [%q]
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "tcp"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = %d
|
||||||
|
remotePort = %d
|
||||||
|
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
||||||
|
|
||||||
|
serverConfigPath := f.GenerateConfigFile(serverConf)
|
||||||
|
clientConfigPath := f.GenerateConfigFile(clientConf)
|
||||||
|
|
||||||
|
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should work with both server and client tokenSource", func() {
|
||||||
|
execValue := "exec-shared-value"
|
||||||
|
scriptPath := createExecTokenScript("shared_token_exec.sh")
|
||||||
|
|
||||||
|
serverPort := f.AllocPort()
|
||||||
|
remotePort := f.AllocPort()
|
||||||
|
|
||||||
|
serverConf := fmt.Sprintf(`
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = %d
|
||||||
|
|
||||||
|
auth.tokenSource.type = "exec"
|
||||||
|
auth.tokenSource.exec.command = %q
|
||||||
|
auth.tokenSource.exec.args = [%q]
|
||||||
|
`, serverPort, scriptPath, execValue)
|
||||||
|
|
||||||
|
clientConf := fmt.Sprintf(`
|
||||||
|
serverAddr = "127.0.0.1"
|
||||||
|
serverPort = %d
|
||||||
|
loginFailExit = false
|
||||||
|
|
||||||
|
auth.tokenSource.type = "exec"
|
||||||
|
auth.tokenSource.exec.command = %q
|
||||||
|
auth.tokenSource.exec.args = [%q]
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "tcp"
|
||||||
|
type = "tcp"
|
||||||
|
localPort = %d
|
||||||
|
remotePort = %d
|
||||||
|
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
||||||
|
|
||||||
|
serverConfigPath := f.GenerateConfigFile(serverConf)
|
||||||
|
clientConfigPath := f.GenerateConfigFile(clientConf)
|
||||||
|
|
||||||
|
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should fail validation without allow-unsafe", func() {
|
||||||
|
execValue := "exec-unsafe-value"
|
||||||
|
scriptPath := createExecTokenScript("unsafe_token_exec.sh")
|
||||||
|
|
||||||
|
serverPort := f.AllocPort()
|
||||||
|
serverConf := fmt.Sprintf(`
|
||||||
|
bindAddr = "0.0.0.0"
|
||||||
|
bindPort = %d
|
||||||
|
|
||||||
|
auth.tokenSource.type = "exec"
|
||||||
|
auth.tokenSource.exec.command = %q
|
||||||
|
auth.tokenSource.exec.args = [%q]
|
||||||
|
`, serverPort, scriptPath, execValue)
|
||||||
|
|
||||||
|
serverConfigPath := f.GenerateConfigFile(serverConf)
|
||||||
|
|
||||||
|
_, output, err := f.RunFrps("verify", "-c", serverConfigPath)
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
framework.ExpectContainSubstring(output, "unsafe feature \"TokenSourceExec\" is not enabled")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package features
|
package features
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/onsi/ginkgo/v2"
|
"github.com/onsi/ginkgo/v2"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/test/e2e/framework"
|
"github.com/fatedier/frp/test/e2e/framework"
|
||||||
"github.com/fatedier/frp/test/e2e/framework/consts"
|
"github.com/fatedier/frp/test/e2e/framework/consts"
|
||||||
"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
|
"github.com/fatedier/frp/test/e2e/mock/server/httpserver"
|
||||||
@@ -112,6 +114,80 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
|
|||||||
|
|
||||||
framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
|
framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.It("HTTPS", func() {
|
||||||
|
vhostHTTPSPort := f.AllocPort()
|
||||||
|
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
|
||||||
|
vhostHTTPSPort = %d
|
||||||
|
`, vhostHTTPSPort)
|
||||||
|
clientConf := consts.DefaultClientConfig
|
||||||
|
|
||||||
|
tlsConfig, err := transport.NewServerTLSConfig("", "", "")
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
fooPort := f.AllocPort()
|
||||||
|
fooServer := httpserver.New(
|
||||||
|
httpserver.WithBindPort(fooPort),
|
||||||
|
httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("foo"))),
|
||||||
|
httpserver.WithTLSConfig(tlsConfig),
|
||||||
|
)
|
||||||
|
f.RunServer("", fooServer)
|
||||||
|
|
||||||
|
barPort := f.AllocPort()
|
||||||
|
barServer := httpserver.New(
|
||||||
|
httpserver.WithBindPort(barPort),
|
||||||
|
httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte("bar"))),
|
||||||
|
httpserver.WithTLSConfig(tlsConfig),
|
||||||
|
)
|
||||||
|
f.RunServer("", barServer)
|
||||||
|
|
||||||
|
clientConf += fmt.Sprintf(`
|
||||||
|
[[proxies]]
|
||||||
|
name = "foo"
|
||||||
|
type = "https"
|
||||||
|
localPort = %d
|
||||||
|
customDomains = ["example.com"]
|
||||||
|
loadBalancer.group = "test"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "bar"
|
||||||
|
type = "https"
|
||||||
|
localPort = %d
|
||||||
|
customDomains = ["example.com"]
|
||||||
|
loadBalancer.group = "test"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
`, fooPort, barPort)
|
||||||
|
|
||||||
|
f.RunProcesses([]string{serverConf}, []string{clientConf})
|
||||||
|
|
||||||
|
fooCount := 0
|
||||||
|
barCount := 0
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
framework.NewRequestExpect(f).
|
||||||
|
Explain("times " + strconv.Itoa(i)).
|
||||||
|
Port(vhostHTTPSPort).
|
||||||
|
RequestModify(func(r *request.Request) {
|
||||||
|
r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{
|
||||||
|
ServerName: "example.com",
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
|
}).
|
||||||
|
Ensure(func(resp *request.Response) bool {
|
||||||
|
switch string(resp.Content) {
|
||||||
|
case "foo":
|
||||||
|
fooCount++
|
||||||
|
case "bar":
|
||||||
|
barCount++
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ginkgo.Describe("Health Check", func() {
|
ginkgo.Describe("Health Check", func() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module.exports = {
|
|||||||
'vue/multi-word-component-names': [
|
'vue/multi-word-component-names': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
ignores: ['Traffic'],
|
ignores: ['Traffic', 'Proxies', 'Clients'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
25
web/frps/components.d.ts
vendored
25
web/frps/components.d.ts
vendored
@@ -7,37 +7,38 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ClientCard: typeof import('./src/components/ClientCard.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
|
||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||||
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElText: typeof import('element-plus/es')['ElText']
|
ElText: typeof import('element-plus/es')['ElText']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
LongSpan: typeof import('./src/components/LongSpan.vue')['default']
|
|
||||||
ProxiesHTTP: typeof import('./src/components/ProxiesHTTP.vue')['default']
|
|
||||||
ProxiesHTTPS: typeof import('./src/components/ProxiesHTTPS.vue')['default']
|
|
||||||
ProxiesSTCP: typeof import('./src/components/ProxiesSTCP.vue')['default']
|
|
||||||
ProxiesSUDP: typeof import('./src/components/ProxiesSUDP.vue')['default']
|
|
||||||
ProxiesTCP: typeof import('./src/components/ProxiesTCP.vue')['default']
|
|
||||||
ProxiesTCPMux: typeof import('./src/components/ProxiesTCPMux.vue')['default']
|
|
||||||
ProxiesUDP: typeof import('./src/components/ProxiesUDP.vue')['default']
|
|
||||||
ProxyView: typeof import('./src/components/ProxyView.vue')['default']
|
|
||||||
ProxyViewExpand: typeof import('./src/components/ProxyViewExpand.vue')['default']
|
ProxyViewExpand: typeof import('./src/components/ProxyViewExpand.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
ServerOverview: typeof import('./src/components/ServerOverview.vue')['default']
|
StatCard: typeof import('./src/components/StatCard.vue')['default']
|
||||||
Traffic: typeof import('./src/components/Traffic.vue')['default']
|
Traffic: typeof import('./src/components/Traffic.vue')['default']
|
||||||
}
|
}
|
||||||
|
export interface ComponentCustomProperties {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>frps dashboard</title>
|
<title>frp server</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -11,28 +11,30 @@
|
|||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/humanize-plus": "^1.8.0",
|
"element-plus": "^2.13.0",
|
||||||
"echarts": "^5.4.3",
|
"vue": "^3.5.26",
|
||||||
"element-plus": "^2.5.3",
|
"vue-router": "^4.6.4"
|
||||||
"humanize-plus": "^1.8.2",
|
|
||||||
"vue": "^3.4.15",
|
|
||||||
"vue-router": "^4.2.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.7.2",
|
"@rushstack/eslint-patch": "^1.15.0",
|
||||||
"@types/node": "^18.11.12",
|
"@types/node": "24",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"@vueuse/core": "^14.1.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-vue": "^9.21.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "~5.3.3",
|
"sass": "^1.97.2",
|
||||||
|
"terser": "^5.44.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
|
"unplugin-element-plus": "^0.11.2",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^5.0.12",
|
"vite": "^7.3.0",
|
||||||
"vue-tsc": "^1.8.27"
|
"vite-svg-loader": "^5.1.0",
|
||||||
|
"vue-tsc": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +1,300 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="grid-content header-color">
|
<header class="header">
|
||||||
<div class="header-content">
|
<div class="header-top">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<a href="#">frp</a>
|
<a href="#" @click.prevent="router.push('/')">frp</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dark-switch">
|
<div class="header-actions">
|
||||||
|
<a
|
||||||
|
class="github-link"
|
||||||
|
href="https://github.com/fatedier/frp"
|
||||||
|
target="_blank"
|
||||||
|
aria-label="GitHub"
|
||||||
|
>
|
||||||
|
<GitHubIcon class="github-icon" />
|
||||||
|
</a>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="darkmodeSwitch"
|
v-model="darkmodeSwitch"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
active-text="Dark"
|
:active-icon="Moon"
|
||||||
inactive-text="Light"
|
:inactive-icon="Sunny"
|
||||||
@change="toggleDark"
|
@change="toggleDark"
|
||||||
style="
|
class="theme-switch"
|
||||||
--el-switch-on-color: #444452;
|
|
||||||
--el-switch-off-color: #589ef8;
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="header-nav">
|
||||||
|
<el-menu
|
||||||
|
:default-active="currentRoute"
|
||||||
|
mode="horizontal"
|
||||||
|
:ellipsis="false"
|
||||||
|
@select="handleSelect"
|
||||||
|
class="nav-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">Overview</el-menu-item>
|
||||||
|
<el-menu-item index="/clients">Clients</el-menu-item>
|
||||||
|
<el-menu-item index="/proxies">Proxies</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<main id="content">
|
||||||
<el-row>
|
<router-view></router-view>
|
||||||
<el-col id="side-nav" :xs="24" :md="4">
|
</main>
|
||||||
<el-menu
|
|
||||||
default-active="/"
|
|
||||||
mode="vertical"
|
|
||||||
theme="light"
|
|
||||||
router="false"
|
|
||||||
@select="handleSelect"
|
|
||||||
>
|
|
||||||
<el-menu-item index="/">Overview</el-menu-item>
|
|
||||||
<el-sub-menu index="/proxies">
|
|
||||||
<template #title>
|
|
||||||
<span>Proxies</span>
|
|
||||||
</template>
|
|
||||||
<el-menu-item index="/proxies/tcp">TCP</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/udp">UDP</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/http">HTTP</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/https">HTTPS</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/tcpmux">TCPMUX</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/stcp">STCP</el-menu-item>
|
|
||||||
<el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
|
|
||||||
</el-sub-menu>
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useDark, useToggle } from '@vueuse/core'
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||||
|
import GitHubIcon from './assets/icons/github.svg?component'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
const darkmodeSwitch = ref(isDark)
|
const darkmodeSwitch = ref(isDark)
|
||||||
const toggleDark = useToggle(isDark)
|
const toggleDark = useToggle(isDark)
|
||||||
|
|
||||||
const handleSelect = (key: string) => {
|
const currentRoute = computed(() => {
|
||||||
if (key == '') {
|
// Normalize /proxies/:type to /proxies for menu highlighting
|
||||||
window.open('https://github.com/fatedier/frp')
|
if (route.path.startsWith('/proxies')) {
|
||||||
|
return '/proxies'
|
||||||
}
|
}
|
||||||
|
return route.path
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelect = (key: string) => {
|
||||||
|
router.push(key)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
|
font-family:
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
Helvetica Neue,
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
#app {
|
||||||
width: 100%;
|
min-height: 100vh;
|
||||||
height: 60px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-color {
|
html.dark #app {
|
||||||
background: #58b7ff;
|
background: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark .header-color {
|
.header {
|
||||||
background: #395c74;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
html.dark .header {
|
||||||
|
background: #1e1e2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
height: 48px;
|
||||||
#content {
|
padding: 0 32px;
|
||||||
margin-top: 20px;
|
|
||||||
padding-right: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand a {
|
.brand a {
|
||||||
color: #fff;
|
color: #303133;
|
||||||
background-color: transparent;
|
font-size: 20px;
|
||||||
margin-left: 20px;
|
font-weight: 700;
|
||||||
line-height: 25px;
|
|
||||||
font-size: 25px;
|
|
||||||
padding: 15px 15px;
|
|
||||||
height: 30px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-switch {
|
html.dark .brand a {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand a:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
flex-grow: 1;
|
gap: 16px;
|
||||||
padding-right: 40px;
|
}
|
||||||
|
|
||||||
|
.github-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover {
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .github-link:hover {
|
||||||
|
background: #2a2a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #606266;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-link:hover .github-icon {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .github-icon {
|
||||||
|
color: #a0a3ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .github-link:hover .github-icon {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch {
|
||||||
|
--el-switch-on-color: #2c2c3a;
|
||||||
|
--el-switch-off-color: #f2f2f2;
|
||||||
|
--el-switch-border-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch .el-switch__core .el-switch__inner .el-icon {
|
||||||
|
color: #909399 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 32px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .header-nav {
|
||||||
|
border-bottom-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu .el-menu-item,
|
||||||
|
.nav-menu .el-sub-menu__title {
|
||||||
|
position: relative;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
color: #666 !important;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
margin: 7px 0;
|
||||||
|
transition:
|
||||||
|
background 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu > .el-menu-item,
|
||||||
|
.nav-menu > .el-sub-menu {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu > .el-sub-menu {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-menu .el-menu-item,
|
||||||
|
html.dark .nav-menu .el-sub-menu__title {
|
||||||
|
color: #888 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu .el-menu-item:hover,
|
||||||
|
.nav-menu .el-sub-menu__title:hover {
|
||||||
|
background: #f2f2f2 !important;
|
||||||
|
color: #171717 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-menu .el-menu-item:hover,
|
||||||
|
html.dark .nav-menu .el-sub-menu__title:hover {
|
||||||
|
background: #2a2a3c !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu .el-menu-item.is-active {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #171717 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu .el-menu-item.is-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -3px;
|
||||||
|
height: 2px;
|
||||||
|
background: #171717;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu .el-menu-item.is-active:hover {
|
||||||
|
background: #f2f2f2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-menu .el-menu-item.is-active {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-menu .el-menu-item.is-active::after {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .nav-menu .el-menu-item.is-active:hover {
|
||||||
|
background: #2a2a3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 40px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-top {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand a {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
10
web/frps/src/api/client.ts
Normal file
10
web/frps/src/api/client.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { http } from './http'
|
||||||
|
import type { ClientInfoData } from '../types/client'
|
||||||
|
|
||||||
|
export const getClients = () => {
|
||||||
|
return http.get<ClientInfoData[]>('../api/clients')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getClient = (key: string) => {
|
||||||
|
return http.get<ClientInfoData>(`../api/clients/${key}`)
|
||||||
|
}
|
||||||
50
web/frps/src/api/http.ts
Normal file
50
web/frps/src/api/http.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// http.ts - Base HTTP client
|
||||||
|
|
||||||
|
class HTTPError extends Error {
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
|
||||||
|
constructor(status: number, statusText: string, message?: string) {
|
||||||
|
super(message || statusText)
|
||||||
|
this.status = status
|
||||||
|
this.statusText = statusText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const defaultOptions: RequestInit = {
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...defaultOptions, ...options })
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new HTTPError(response.status, response.statusText, `HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty response (e.g. 204 No Content)
|
||||||
|
if (response.status === 204) {
|
||||||
|
return {} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const http = {
|
||||||
|
get: <T>(url: string, options?: RequestInit) => request<T>(url, { ...options, method: 'GET' }),
|
||||||
|
post: <T>(url: string, body?: any, options?: RequestInit) =>
|
||||||
|
request<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}),
|
||||||
|
put: <T>(url: string, body?: any, options?: RequestInit) =>
|
||||||
|
request<T>(url, {
|
||||||
|
...options,
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}),
|
||||||
|
delete: <T>(url: string, options?: RequestInit) => request<T>(url, { ...options, method: 'DELETE' }),
|
||||||
|
}
|
||||||
18
web/frps/src/api/proxy.ts
Normal file
18
web/frps/src/api/proxy.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { http } from './http'
|
||||||
|
import type { GetProxyResponse, ProxyStatsInfo, TrafficResponse } from '../types/proxy'
|
||||||
|
|
||||||
|
export const getProxiesByType = (type: string) => {
|
||||||
|
return http.get<GetProxyResponse>(`../api/proxy/${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProxy = (type: string, name: string) => {
|
||||||
|
return http.get<ProxyStatsInfo>(`../api/proxy/${type}/${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProxyTraffic = (name: string) => {
|
||||||
|
return http.get<TrafficResponse>(`../api/traffic/${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearOfflineProxies = () => {
|
||||||
|
return http.delete('../api/proxies?status=offline')
|
||||||
|
}
|
||||||
6
web/frps/src/api/server.ts
Normal file
6
web/frps/src/api/server.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { http } from './http'
|
||||||
|
import type { ServerInfo } from '../types/server'
|
||||||
|
|
||||||
|
export const getServerInfo = () => {
|
||||||
|
return http.get<ServerInfo>('../api/serverinfo')
|
||||||
|
}
|
||||||
89
web/frps/src/assets/css/custom.css
Normal file
89
web/frps/src/assets/css/custom.css
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.el-form-item span {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-table-expand {
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-table-expand .el-form-item__label{
|
||||||
|
width: 90px;
|
||||||
|
color: #99a9bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-table-expand .el-form-item {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .el-table__expanded-cell {
|
||||||
|
padding: 20px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern styles */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
.el-button,
|
||||||
|
.el-card,
|
||||||
|
.el-input,
|
||||||
|
.el-select,
|
||||||
|
.el-tag {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effects */
|
||||||
|
.el-card:hover {
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page headers */
|
||||||
|
.el-page-header {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-page-header__title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better form layouts */
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.el-row {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-col {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
web/frps/src/assets/css/dark.css
Normal file
58
web/frps/src/assets/css/dark.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
html.dark {
|
||||||
|
--el-bg-color: #1e1e2e;
|
||||||
|
--el-fill-color-blank: #1e1e2e;
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark body {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbar */
|
||||||
|
html.dark ::-webkit-scrollbar-track {
|
||||||
|
background: #27293d;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4a4d6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode cards */
|
||||||
|
html.dark .el-card {
|
||||||
|
background-color: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode inputs */
|
||||||
|
html.dark .el-input__wrapper {
|
||||||
|
background-color: #27293d;
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-input__inner {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode table */
|
||||||
|
html.dark .el-table {
|
||||||
|
background-color: #27293d;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-table th {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-table tr {
|
||||||
|
background-color: #27293d;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||||
|
background-color: #1e1e2e;
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.el-form-item span {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand {
|
|
||||||
font-size: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand .el-form-item__label{
|
|
||||||
width: 90px;
|
|
||||||
color: #99a9bf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proxy-table-expand .el-form-item {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table .el-table__expanded-cell {
|
|
||||||
padding: 20px 50px;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
html.dark {
|
|
||||||
--el-bg-color: #343432;
|
|
||||||
--el-fill-color-blank: #343432;
|
|
||||||
background-color: #343432;
|
|
||||||
}
|
|
||||||
3
web/frps/src/assets/icons/github.svg
Normal file
3
web/frps/src/assets/icons/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 671 B |
229
web/frps/src/components/ClientCard.vue
Normal file
229
web/frps/src/components/ClientCard.vue
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="client-card" shadow="hover" :body-style="{ padding: '20px' }">
|
||||||
|
<div class="client-header">
|
||||||
|
<div class="client-status">
|
||||||
|
<span class="status-dot" :class="statusClass"></span>
|
||||||
|
<span class="client-name">{{ client.displayName }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="client.statusColor" size="small">
|
||||||
|
{{ client.online ? 'Online' : 'Offline' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="client-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<el-icon class="info-icon"><Monitor /></el-icon>
|
||||||
|
<span class="info-label">Hostname:</span>
|
||||||
|
<span class="info-value">{{ client.hostname || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row" v-if="client.user">
|
||||||
|
<el-icon class="info-icon"><User /></el-icon>
|
||||||
|
<span class="info-label">User:</span>
|
||||||
|
<span class="info-value">{{ client.user }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<el-icon class="info-icon"><Key /></el-icon>
|
||||||
|
<span class="info-label">Run ID:</span>
|
||||||
|
<span class="info-value monospace">{{ client.runId }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row" v-if="client.firstConnectedAt">
|
||||||
|
<el-icon class="info-icon"><Clock /></el-icon>
|
||||||
|
<span class="info-label">First Connected:</span>
|
||||||
|
<span class="info-value">{{ client.firstConnectedAgo }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row" v-if="client.online">
|
||||||
|
<el-icon class="info-icon"><Clock /></el-icon>
|
||||||
|
<span class="info-label">Last Connected:</span>
|
||||||
|
<span class="info-value">{{ client.lastConnectedAgo }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row" v-if="!client.online && client.disconnectedAt">
|
||||||
|
<el-icon class="info-icon"><CircleClose /></el-icon>
|
||||||
|
<span class="info-label">Disconnected:</span>
|
||||||
|
<span class="info-value">{{ client.disconnectedAgo }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="client-metas" v-if="client.metasArray.length > 0">
|
||||||
|
<div class="metas-label">Metadata:</div>
|
||||||
|
<div class="metas-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="meta in client.metasArray"
|
||||||
|
:key="meta.key"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="meta-tag"
|
||||||
|
>
|
||||||
|
{{ meta.key }}: {{ meta.value }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Monitor, User, Key, Clock, CircleClose } from '@element-plus/icons-vue'
|
||||||
|
import type { Client } from '../utils/client'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
client: Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
return `status-${props.client.statusColor}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.client-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .client-card {
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
background: #27293d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .client-header {
|
||||||
|
border-bottom-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background-color: #67c23a;
|
||||||
|
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
box-shadow: 0 0 0 0 rgba(230, 162, 60, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-danger {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .client-name {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .info-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .info-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #606266;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .info-value {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-metas {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .client-metas {
|
||||||
|
border-top-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metas-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .metas-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metas-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-tooltip :content="content" placement="top">
|
|
||||||
<span v-show="content.length > length"
|
|
||||||
>{{ content.slice(0, length) }}...</span
|
|
||||||
>
|
|
||||||
</el-tooltip>
|
|
||||||
<span v-show="content.length < 30">{{ content }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
content: string
|
|
||||||
length: number
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="http" @refresh="fetchData"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { HTTPProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<HTTPProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
let vhostHTTPPort: number
|
|
||||||
let subdomainHost: string
|
|
||||||
fetch('../api/serverinfo', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
vhostHTTPPort = json.vhostHTTPPort
|
|
||||||
subdomainHost = json.subdomainHost
|
|
||||||
if (vhostHTTPPort == null || vhostHTTPPort == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetch('../api/proxy/http', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(
|
|
||||||
new HTTPProxy(proxyStats, vhostHTTPPort, subdomainHost)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="https" @refresh="fetchData"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { HTTPSProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<HTTPSProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
let vhostHTTPSPort: number
|
|
||||||
let subdomainHost: string
|
|
||||||
fetch('../api/serverinfo', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
vhostHTTPSPort = json.vhostHTTPSPort
|
|
||||||
subdomainHost = json.subdomainHost
|
|
||||||
if (vhostHTTPSPort == null || vhostHTTPSPort == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetch('../api/proxy/https', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(
|
|
||||||
new HTTPSProxy(proxyStats, vhostHTTPSPort, subdomainHost)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="stcp" @refresh="fetchData"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { STCPProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<STCPProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
fetch('../api/proxy/stcp', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(new STCPProxy(proxyStats))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="sudp" @refresh="fetchData"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { SUDPProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<SUDPProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
fetch('../api/proxy/sudp', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(new SUDPProxy(proxyStats))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="tcp" @refresh="fetchData" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { TCPProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<TCPProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
fetch('../api/proxy/tcp', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(new TCPProxy(proxyStats))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="tcpmux" @refresh="fetchData" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { TCPMuxProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<TCPMuxProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
let tcpmuxHTTPConnectPort: number
|
|
||||||
let subdomainHost: string
|
|
||||||
fetch('../api/serverinfo', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
tcpmuxHTTPConnectPort = json.tcpmuxHTTPConnectPort
|
|
||||||
subdomainHost = json.subdomainHost
|
|
||||||
|
|
||||||
fetch('../api/proxy/tcpmux', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(new TCPMuxProxy(proxyStats, tcpmuxHTTPConnectPort, subdomainHost))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ProxyView :proxies="proxies" proxyType="udp" @refresh="fetchData"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { UDPProxy } from '../utils/proxy.js'
|
|
||||||
import ProxyView from './ProxyView.vue'
|
|
||||||
|
|
||||||
let proxies = ref<UDPProxy[]>([])
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
fetch('../api/proxy/udp', { credentials: 'include' })
|
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
proxies.value = []
|
|
||||||
for (let proxyStats of json.proxies) {
|
|
||||||
proxies.value.push(new UDPProxy(proxyStats))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-page-header
|
|
||||||
:icon="null"
|
|
||||||
style="width: 100%; margin-left: 30px; margin-bottom: 20px"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<span>{{ proxyType }}</span>
|
|
||||||
</template>
|
|
||||||
<template #content> </template>
|
|
||||||
<template #extra>
|
|
||||||
<div class="flex items-center" style="margin-right: 30px">
|
|
||||||
<el-popconfirm
|
|
||||||
title="Are you sure to clear all data of offline proxies?"
|
|
||||||
@confirm="clearOfflineProxies"
|
|
||||||
>
|
|
||||||
<template #reference>
|
|
||||||
<el-button>ClearOfflineProxies</el-button>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
<el-button @click="$emit('refresh')">Refresh</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-page-header>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
:data="proxies"
|
|
||||||
:default-sort="{ prop: 'name', order: 'ascending' }"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-table-column type="expand">
|
|
||||||
<template #default="props">
|
|
||||||
<ProxyViewExpand :row="props.row" :proxyType="proxyType" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="Name" prop="name" sortable> </el-table-column>
|
|
||||||
<el-table-column label="Port" prop="port" sortable> </el-table-column>
|
|
||||||
<el-table-column label="Connections" prop="conns" sortable>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
label="Traffic In"
|
|
||||||
prop="trafficIn"
|
|
||||||
:formatter="formatTrafficIn"
|
|
||||||
sortable
|
|
||||||
>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
label="Traffic Out"
|
|
||||||
prop="trafficOut"
|
|
||||||
:formatter="formatTrafficOut"
|
|
||||||
sortable
|
|
||||||
>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="ClientVersion" prop="clientVersion" sortable>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="Status" prop="status" sortable>
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag v-if="scope.row.status === 'online'" type="success">{{
|
|
||||||
scope.row.status
|
|
||||||
}}</el-tag>
|
|
||||||
<el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="Operations">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:name="scope.row.name"
|
|
||||||
style="margin-bottom: 10px"
|
|
||||||
@click="dialogVisibleName = scope.row.name; dialogVisible = true"
|
|
||||||
>Traffic
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
destroy-on-close="true"
|
|
||||||
:title="dialogVisibleName"
|
|
||||||
width="700px">
|
|
||||||
<Traffic :proxyName="dialogVisibleName" />
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import * as Humanize from 'humanize-plus'
|
|
||||||
import type { TableColumnCtx } from 'element-plus'
|
|
||||||
import type { BaseProxy } from '../utils/proxy.js'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import ProxyViewExpand from './ProxyViewExpand.vue'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
proxies: BaseProxy[]
|
|
||||||
proxyType: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits(['refresh'])
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const dialogVisibleName = ref("")
|
|
||||||
|
|
||||||
const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
|
|
||||||
return Humanize.fileSize(row.trafficIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
|
|
||||||
return Humanize.fileSize(row.trafficOut)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearOfflineProxies = () => {
|
|
||||||
fetch('../api/proxies?status=offline', {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
ElMessage({
|
|
||||||
message: 'Successfully cleared offline proxies',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
emit('refresh')
|
|
||||||
} else {
|
|
||||||
ElMessage({
|
|
||||||
message: 'Failed to clear offline proxies: ' + res.status + ' ' + res.statusText,
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
ElMessage({
|
|
||||||
message: 'Failed to clear offline proxies: ' + err.message,
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.el-page-header__title {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -60,19 +60,18 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div v-if="row.annotations && row.annotations.size > 0">
|
<div v-if="row.annotations && row.annotations.size > 0">
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<el-text class="title-text" size="large">Annotations</el-text>
|
<el-text class="title-text" size="large">Annotations</el-text>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="item in annotationsArray()">
|
<li v-for="item in annotationsArray()" :key="item.key">
|
||||||
<span class="annotation-key">{{ item.key }}</span>
|
<span class="annotation-key">{{ item.key }}</span>
|
||||||
<span>{{ item.value }}</span>
|
<span>{{ item.value }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
row: any
|
row: any
|
||||||
proxyType: string
|
proxyType: string
|
||||||
@@ -80,13 +79,13 @@ const props = defineProps<{
|
|||||||
|
|
||||||
// annotationsArray returns an array of key-value pairs from the annotations map.
|
// annotationsArray returns an array of key-value pairs from the annotations map.
|
||||||
const annotationsArray = (): Array<{ key: string; value: string }> => {
|
const annotationsArray = (): Array<{ key: string; value: string }> => {
|
||||||
const array: Array<{ key: string; value: any }> = [];
|
const array: Array<{ key: string; value: any }> = []
|
||||||
if (props.row.annotations) {
|
if (props.row.annotations) {
|
||||||
props.row.annotations.forEach((value: any, key: string) => {
|
props.row.annotations.forEach((value: any, key: string) => {
|
||||||
array.push({ key, value });
|
array.push({ key, value })
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
return array;
|
return array
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-row>
|
|
||||||
<el-col :md="12">
|
|
||||||
<div class="source">
|
|
||||||
<el-form
|
|
||||||
label-position="left"
|
|
||||||
label-width="220px"
|
|
||||||
class="server_info"
|
|
||||||
>
|
|
||||||
<el-form-item label="Version">
|
|
||||||
<span>{{ data.version }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="BindPort">
|
|
||||||
<span>{{ data.bindPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="KCP Bind Port" v-if="data.kcpBindPort != 0">
|
|
||||||
<span>{{ data.kcpBindPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0">
|
|
||||||
<span>{{ data.quicBindPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="HTTP Port" v-if="data.vhostHTTPPort != 0">
|
|
||||||
<span>{{ data.vhostHTTPPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="HTTPS Port" v-if="data.vhostHTTPSPort != 0">
|
|
||||||
<span>{{ data.vhostHTTPSPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
label="TCPMux HTTPConnect Port"
|
|
||||||
v-if="data.tcpmuxHTTPConnectPort != 0"
|
|
||||||
>
|
|
||||||
<span>{{ data.tcpmuxHTTPConnectPort }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
label="Subdomain Host"
|
|
||||||
v-if="data.subdomainHost != ''"
|
|
||||||
>
|
|
||||||
<LongSpan :content="data.subdomainHost" :length="30"></LongSpan>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Max PoolCount">
|
|
||||||
<span>{{ data.maxPoolCount }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Max Ports Per Client">
|
|
||||||
<span>{{ data.maxPortsPerClient }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Allow Ports" v-if="data.allowPortsStr != ''">
|
|
||||||
<LongSpan :content="data.allowPortsStr" :length="30"></LongSpan>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="TLS Force" v-if="data.tlsForce === true">
|
|
||||||
<span>{{ data.tlsForce }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="HeartBeat Timeout">
|
|
||||||
<span>{{ data.heartbeatTimeout }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Client Counts">
|
|
||||||
<span>{{ data.clientCounts }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Current Connections">
|
|
||||||
<span>{{ data.curConns }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Proxy Counts">
|
|
||||||
<span>{{ data.proxyCounts }}</span>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :md="12">
|
|
||||||
<div
|
|
||||||
id="traffic"
|
|
||||||
style="width: 400px; height: 250px; margin-bottom: 30px"
|
|
||||||
></div>
|
|
||||||
<div id="proxies" style="width: 400px; height: 250px"></div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { DrawTrafficChart, DrawProxyChart } from '../utils/chart'
|
|
||||||
import LongSpan from './LongSpan.vue'
|
|
||||||
|
|
||||||
let data = ref({
|
|
||||||
version: '',
|
|
||||||
bindPort: 0,
|
|
||||||
kcpBindPort: 0,
|
|
||||||
quicBindPort: 0,
|
|
||||||
vhostHTTPPort: 0,
|
|
||||||
vhostHTTPSPort: 0,
|
|
||||||
tcpmuxHTTPConnectPort: 0,
|
|
||||||
subdomainHost: '',
|
|
||||||
maxPoolCount: 0,
|
|
||||||
maxPortsPerClient: '',
|
|
||||||
allowPortsStr: '',
|
|
||||||
tlsForce: false,
|
|
||||||
heartbeatTimeout: 0,
|
|
||||||
clientCounts: 0,
|
|
||||||
curConns: 0,
|
|
||||||
proxyCounts: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchData = () => {
|
|
||||||
fetch('../api/serverinfo', { credentials: 'include' })
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((json) => {
|
|
||||||
data.value.version = json.version
|
|
||||||
data.value.bindPort = json.bindPort
|
|
||||||
data.value.kcpBindPort = json.kcpBindPort
|
|
||||||
data.value.quicBindPort = json.quicBindPort
|
|
||||||
data.value.vhostHTTPPort = json.vhostHTTPPort
|
|
||||||
data.value.vhostHTTPSPort = json.vhostHTTPSPort
|
|
||||||
data.value.tcpmuxHTTPConnectPort = json.tcpmuxHTTPConnectPort
|
|
||||||
data.value.subdomainHost = json.subdomainHost
|
|
||||||
data.value.maxPoolCount = json.maxPoolCount
|
|
||||||
data.value.maxPortsPerClient = json.maxPortsPerClient
|
|
||||||
if (data.value.maxPortsPerClient == '0') {
|
|
||||||
data.value.maxPortsPerClient = 'no limit'
|
|
||||||
}
|
|
||||||
data.value.allowPortsStr = json.allowPortsStr
|
|
||||||
data.value.tlsForce = json.tlsForce
|
|
||||||
data.value.heartbeatTimeout = json.heartbeatTimeout
|
|
||||||
data.value.clientCounts = json.clientCounts
|
|
||||||
data.value.curConns = json.curConns
|
|
||||||
data.value.proxyCounts = 0
|
|
||||||
if (json.proxyTypeCount != null) {
|
|
||||||
if (json.proxyTypeCount.tcp != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.tcp
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.udp != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.udp
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.http != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.http
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.https != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.https
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.stcp != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.stcp
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.sudp != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.sudp
|
|
||||||
}
|
|
||||||
if (json.proxyTypeCount.xtcp != null) {
|
|
||||||
data.value.proxyCounts += json.proxyTypeCount.xtcp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw chart
|
|
||||||
DrawTrafficChart('traffic', json.totalTrafficIn, json.totalTrafficOut)
|
|
||||||
DrawProxyChart('proxies', json)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
ElMessage({
|
|
||||||
showClose: true,
|
|
||||||
message: 'Get server info from frps failed!',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fetchData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.source {
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: 0.2s;
|
|
||||||
padding-left: 24px;
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server_info {
|
|
||||||
margin-left: 40px;
|
|
||||||
font-size: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server_info .el-form-item__label {
|
|
||||||
color: #99a9bf;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server_info .el-form-item__content {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server_info .el-form-item {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
202
web/frps/src/components/StatCard.vue
Normal file
202
web/frps/src/components/StatCard.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<el-card
|
||||||
|
class="stat-card"
|
||||||
|
:class="{ clickable: !!to }"
|
||||||
|
:body-style="{ padding: '20px' }"
|
||||||
|
shadow="hover"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<div class="stat-card-content">
|
||||||
|
<div class="stat-icon" :class="`icon-${type}`">
|
||||||
|
<component :is="iconComponent" class="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ value }}</div>
|
||||||
|
<div class="stat-label">{{ label }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="to" class="arrow-icon"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="subtitle" class="stat-subtitle">{{ subtitle }}</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Connection,
|
||||||
|
DataAnalysis,
|
||||||
|
Promotion,
|
||||||
|
ArrowRight,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
type?: 'clients' | 'proxies' | 'connections' | 'traffic'
|
||||||
|
subtitle?: string
|
||||||
|
to?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'clients',
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const iconComponent = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'clients':
|
||||||
|
return User
|
||||||
|
case 'proxies':
|
||||||
|
return Connection
|
||||||
|
case 'connections':
|
||||||
|
return DataAnalysis
|
||||||
|
case 'traffic':
|
||||||
|
return Promotion
|
||||||
|
default:
|
||||||
|
return User
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (props.to) {
|
||||||
|
router.push(props.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.clickable:hover .arrow-icon {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .stat-card {
|
||||||
|
border-color: #3a3d5c;
|
||||||
|
background: #27293d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .arrow-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon .icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-clients {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-proxies {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-connections {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-traffic {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .icon-clients {
|
||||||
|
background: linear-gradient(135deg, #818cf8 0%, #a78bfa 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .icon-proxies {
|
||||||
|
background: linear-gradient(135deg, #fb7185 0%, #f43f5e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .icon-connections {
|
||||||
|
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .icon-traffic {
|
||||||
|
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .stat-value {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .stat-label {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-subtitle {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .stat-subtitle {
|
||||||
|
border-top-color: #3a3d5c;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,32 +1,260 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="proxyName" style="width: 600px; height: 400px"></div>
|
<div class="traffic-chart-container" v-loading="loading">
|
||||||
|
<div v-if="!loading && chartData.length > 0" class="chart-wrapper">
|
||||||
|
<div class="y-axis">
|
||||||
|
<div class="y-label">{{ formatFileSize(maxVal) }}</div>
|
||||||
|
<div class="y-label">{{ formatFileSize(maxVal / 2) }}</div>
|
||||||
|
<div class="y-label">0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bars-area">
|
||||||
|
<!-- Grid Lines -->
|
||||||
|
<div class="grid-line top"></div>
|
||||||
|
<div class="grid-line middle"></div>
|
||||||
|
<div class="grid-line bottom"></div>
|
||||||
|
|
||||||
|
<div v-for="(item, index) in chartData" :key="index" class="day-column">
|
||||||
|
<div class="bars-group">
|
||||||
|
<el-tooltip :content="`In: ${formatFileSize(item.in)}`" placement="top">
|
||||||
|
<div
|
||||||
|
class="bar bar-in"
|
||||||
|
:style="{ height: Math.max(item.inPercent, 1) + '%' }"
|
||||||
|
></div>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip :content="`Out: ${formatFileSize(item.out)}`" placement="top">
|
||||||
|
<div
|
||||||
|
class="bar bar-out"
|
||||||
|
:style="{ height: Math.max(item.outPercent, 1) + '%' }"
|
||||||
|
></div>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="date-label">{{ item.date }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div v-if="!loading && chartData.length > 0" class="legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="dot in"></span> Traffic In
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="dot out"></span> Traffic Out
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!loading" description="No traffic data" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { DrawProxyTrafficChart } from '../utils/chart.js'
|
import { formatFileSize } from '../utils/format'
|
||||||
|
import { getProxyTraffic } from '../api/proxy'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
proxyName: string
|
proxyName: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const chartData = ref<Array<{
|
||||||
|
date: string
|
||||||
|
in: number
|
||||||
|
out: number
|
||||||
|
inPercent: number
|
||||||
|
outPercent: number
|
||||||
|
}>>([])
|
||||||
|
const maxVal = ref(0)
|
||||||
|
|
||||||
|
const processData = (trafficIn: number[], trafficOut: number[]) => {
|
||||||
|
// Ensure we have arrays and reverse them (server returns newest first)
|
||||||
|
const inArr = [...(trafficIn || [])].reverse()
|
||||||
|
const outArr = [...(trafficOut || [])].reverse()
|
||||||
|
|
||||||
|
// Pad with zeros if less than 7 days
|
||||||
|
while (inArr.length < 7) inArr.unshift(0)
|
||||||
|
while (outArr.length < 7) outArr.unshift(0)
|
||||||
|
|
||||||
|
// Slice to last 7 entries just in case
|
||||||
|
const finalIn = inArr.slice(-7)
|
||||||
|
const finalOut = outArr.slice(-7)
|
||||||
|
|
||||||
|
// Calculate dates (last 7 days ending today)
|
||||||
|
const dates: string[] = []
|
||||||
|
let d = new Date()
|
||||||
|
d.setDate(d.getDate() - 6)
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
dates.push(`${d.getMonth() + 1}-${d.getDate()}`)
|
||||||
|
d.setDate(d.getDate() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find max value for scaling
|
||||||
|
const maxIn = Math.max(...finalIn)
|
||||||
|
const maxOut = Math.max(...finalOut)
|
||||||
|
maxVal.value = Math.max(maxIn, maxOut, 100) // Minimum scale 100 bytes
|
||||||
|
|
||||||
|
// Build chart data
|
||||||
|
chartData.value = dates.map((date, i) => ({
|
||||||
|
date,
|
||||||
|
in: finalIn[i],
|
||||||
|
out: finalOut[i],
|
||||||
|
inPercent: (finalIn[i] / maxVal.value) * 100,
|
||||||
|
outPercent: (finalOut[i] / maxVal.value) * 100,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = () => {
|
const fetchData = () => {
|
||||||
let url = '../api/traffic/' + props.proxyName
|
loading.value = true
|
||||||
fetch(url, { credentials: 'include' })
|
getProxyTraffic(props.proxyName)
|
||||||
.then((res) => {
|
|
||||||
return res.json()
|
|
||||||
})
|
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
DrawProxyTrafficChart(props.proxyName, json.trafficIn, json.trafficOut)
|
processData(json.trafficIn, json.trafficOut)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
showClose: true,
|
showClose: true,
|
||||||
message: 'Get traffic info failed!' + err,
|
message: 'Get traffic info failed! ' + err,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fetchData()
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style></style>
|
|
||||||
|
<style scoped>
|
||||||
|
.traffic-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-axis {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
padding-bottom: 24px; /* Align with bars area excluding date labels */
|
||||||
|
height: calc(100% - 24px); /* Subtract date label height approx */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bars-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: 24px; /* Space for date labels */
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .grid-line {
|
||||||
|
background-color: #3a3d5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-line.top { top: 0; }
|
||||||
|
.grid-line.middle { top: 50%; transform: translateY(-50%); }
|
||||||
|
.grid-line.bottom { bottom: 24px; } /* Align with bottom of bars */
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bars-group {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-in {
|
||||||
|
background-color: #5470c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-out {
|
||||||
|
background-color: #91cc75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .legend-item {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.in { background-color: #5470c6; }
|
||||||
|
.dot.out { background-color: #91cc75; }
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import './assets/custom.css'
|
import './assets/css/custom.css'
|
||||||
import './assets/dark.css'
|
import './assets/css/dark.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user