Compare commits

...

15 Commits

Author SHA1 Message Date
fatedier 4e8e9e1dec
Merge pull request #4205 from fatedier/dev
bump version
2024-05-07 18:08:48 +08:00
fatedier 92cb0b30c2
update version (#4204) 2024-05-07 18:05:36 +08:00
fatedier e81b36c5ba
support responseHeaders.set for proxy type http (#4192) 2024-04-29 15:53:45 +08:00
Weltolk d0d396becb
Update README.md (#4190) 2024-04-29 11:50:56 +08:00
fatedier ee3892798d
change default value of heartbeat interval and timeout when tcpmux enabled (#4186) 2024-04-28 20:48:44 +08:00
fatedier 405969085f
client: add StatusExporter in service (#4182) 2024-04-25 20:20:39 +08:00
fatedier c1893ee1b4
adjust arm compilation configuration (#4181) 2024-04-25 13:08:41 +08:00
ddscentral eaae212d2d
Makefile.cross-compiles: Fix softfloat flag not being honored for mipsle. (#4176) 2024-04-22 19:50:58 +08:00
fatedier 885278c045
README add releated projects (#4167) 2024-04-17 20:41:14 +08:00
fatedier 2626d6ed92
support linux/arm v6 (#4154) 2024-04-12 21:21:28 +08:00
fatedier f3a71bc08f
show tcpmux proxies on the frps dashboard (#4152) 2024-04-11 22:40:42 +08:00
fatedier dd7e2e8473
return 504 instead of 404 for proxy type http request timeout (#4151) 2024-04-11 20:19:08 +08:00
gopherfarm 07946e9752
fix: revert gorilla/websocket from 1.5.1 to 1.5.0 (#4149) 2024-04-11 11:01:02 +08:00
fatedier e52727e01c
update golib (#4142) 2024-04-10 12:05:22 +08:00
fatedier ba937e9fbf
update README (#4139) 2024-04-09 15:03:32 +08:00
48 changed files with 494 additions and 244 deletions

View File

@ -1,6 +1,6 @@
service:
golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly
run:
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
@ -86,12 +86,8 @@ linters-settings:
severity: "low"
confidence: "low"
excludes:
- G102
- G112
- G306
- G401
- G402
- G404
- G501
issues:

View File

@ -2,22 +2,34 @@ export PATH := $(PATH):`go env GOPATH`/bin
export GO111MODULE=on
LDFLAGS := -s -w
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
all: build
build: app
app:
@$(foreach n, $(os-archs),\
os=$(shell echo "$(n)" | cut -d : -f 1);\
arch=$(shell echo "$(n)" | cut -d : -f 2);\
gomips=$(shell echo "$(n)" | cut -d : -f 3);\
target_suffix=$${os}_$${arch};\
echo "Build $${os}-$${arch}...";\
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc;\
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps;\
echo "Build $${os}-$${arch} done";\
@$(foreach n, $(os-archs), \
os=$(shell echo "$(n)" | cut -d : -f 1); \
arch=$(shell echo "$(n)" | cut -d : -f 2); \
extra=$(shell echo "$(n)" | cut -d : -f 3); \
flags=''; \
target_suffix=$${os}_$${arch}; \
if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
if [ "$${extra}" = "7" ]; then \
flags=GOARM=7; \
target_suffix=$${os}_arm_hf; \
elif [ "$${extra}" = "5" ]; then \
flags=GOARM=5; \
target_suffix=$${os}_arm; \
fi; \
elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
flags=GOMIPS=$${extra}; \
fi; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
)
@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe

View File

@ -82,6 +82,7 @@ frp also offers a P2P connect mode.
* [Client Plugins](#client-plugins)
* [Server Manage Plugins](#server-manage-plugins)
* [SSH Tunnel Gateway](#ssh-tunnel-gateway)
* [Releated Projects](#releated-projects)
* [Contributing](#contributing)
* [Donation](#donation)
* [GitHub Sponsors](#github-sponsors)
@ -351,7 +352,6 @@ You may substitute `https2https` for the plugin, and point the `localAddr` to a
# frpc.toml
serverAddr = "x.x.x.x"
serverPort = 7000
vhostHTTPSPort = 443
[[proxies]]
name = "test_https2http"
@ -804,7 +804,7 @@ You can disable this feature by modify `frps.toml` and `frpc.toml`:
```toml
# frps.toml and frpc.toml, must be same
tcpMux = false
transport.tcpMux = false
```
### Support KCP Protocol
@ -983,7 +983,7 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com
### Setting other HTTP Headers
Similar to `Host`, You can override other HTTP request headers with proxy type `http`.
Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`.
```toml
# frpc.toml
@ -995,15 +995,16 @@ localPort = 80
customDomains = ["test.example.com"]
hostHeaderRewrite = "dev.example.com"
requestHeaders.set.x-from-where = "frp"
responseHeaders.set.foo = "bar"
```
In this example, it will set header `x-from-where: frp` in the HTTP request.
In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response.
### Get Real IP
#### HTTP X-Forwarded-For
This feature is for http proxy only.
This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled.
You can get user's real IP from HTTP request headers `X-Forwarded-For`.
@ -1244,6 +1245,11 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote
Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
## Releated Projects
* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources.
## Contributing
Interested in getting involved? We would like to help you!

View File

@ -72,7 +72,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
* 贡献代码请提交 PR 至 dev 分支master 分支仅用于发布稳定可用版本。
* 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
**提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
## 关联项目
* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
## 赞助
@ -93,7 +98,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
![zsxq](/doc/pic/zsxq.jpg)
### 微信支付捐赠
![donate-wechatpay](/doc/pic/donate-wechatpay.png)

View File

@ -1,7 +1,14 @@
### Notable Changes
We have optimized the heartbeat mechanism when tcpmux is enabled (enabled by default). The default value of `heartbeatInterval` has been adjusted to -1. This update ensures that when tcpmux is active, the client does not send additional heartbeats to the server. Since tcpmux incorporates its own heartbeat system, this change effectively reduces unnecessary data consumption, streamlining communication efficiency between client and server.
When connecting to frps versions older than v0.39.0 might encounter compatibility issues due to changes in the heartbeat mechanism. As a temporary workaround, setting the `heartbeatInterval` to 30 can help maintain stable connectivity with these older versions. We recommend updating to the latest frps version to leverage full functionality and improvements.
### Features
* `https2http` and `https2https` plugin now supports `X-Forwared-For` header.
* Show tcpmux proxies on the frps dashboard.
* `http` proxy can modify the response header. For example, `responseHeaders.set.foo = "bar"` will add a new header `foo: bar` to the response.
### Fixes
* `X-Forwared-For` header is now correctly set in the request to the backend server for proxy type http.
* When an HTTP proxy request times out, it returns 504 instead of 404 now.

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-Q42Pu2_S.js"></script>
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
</head>

View File

@ -253,7 +253,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
return
}
if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil {
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
res.Code = 500
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
log.Warnf("%s", res.Msg)

View File

@ -24,7 +24,7 @@ import (
"sync"
"time"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
fmux "github.com/hashicorp/yamux"
quic "github.com/quic-go/quic-go"
"github.com/samber/lo"
@ -169,44 +169,44 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
}
}
proxyType, addr, auth, err := libdial.ParseProxyURL(c.cfg.Transport.ProxyURL)
proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
if err != nil {
xl.Errorf("fail to parse proxy url")
return nil, err
}
dialOptions := []libdial.DialOption{}
dialOptions := []libnet.DialOption{}
protocol := c.cfg.Transport.Protocol
switch protocol {
case "websocket":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
case "wss":
protocol = "tcp"
dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfigAndPriority(100, tlsConfig))
// Make sure that if it is wss, the websocket hook is executed after the tls hook.
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
default:
dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
}))
dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
}
if c.cfg.Transport.ConnectServerLocalIP != "" {
dialOptions = append(dialOptions, libdial.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
}
dialOptions = append(dialOptions,
libdial.WithProtocol(protocol),
libdial.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
libdial.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
libdial.WithProxy(proxyType, addr),
libdial.WithProxyAuth(auth),
libnet.WithProtocol(protocol),
libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
libnet.WithProxy(proxyType, addr),
libnet.WithProxyAuth(auth),
)
conn, err := libdial.DialContext(
conn, err := libnet.DialContext(
c.ctx,
net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
dialOptions...,

View File

@ -20,8 +20,6 @@ import (
"sync/atomic"
"time"
"github.com/samber/lo"
"github.com/fatedier/frp/client/proxy"
"github.com/fatedier/frp/client/visitor"
"github.com/fatedier/frp/pkg/auth"
@ -236,10 +234,8 @@ func (ctl *Control) registerMsgHandlers() {
func (ctl *Control) heartbeatWorker() {
xl := ctl.xl
// TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
// send heartbeat to server
// Send heartbeat to server.
sendHeartBeat := func() (bool, error) {
xl.Debugf("send heartbeat to server")
pingMsg := &msg.Ping{}
@ -263,10 +259,8 @@ func (ctl *Control) heartbeatWorker() {
)
}
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 &&
!lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) {
// Check heartbeat timeout.
if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
go wait.Until(func() {
if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
xl.Warnf("heartbeat timeout")

View File

@ -25,7 +25,7 @@ import (
"time"
libio "github.com/fatedier/golib/io"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
pp "github.com/pires/go-proxyproto"
"golang.org/x/time/rate"
@ -197,9 +197,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
return
}
localConn, err := libdial.Dial(
localConn, err := libnet.Dial(
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
libdial.WithTimeout(10*time.Second),
libnet.WithTimeout(10*time.Second),
)
if err != nil {
workConn.Close()

View File

@ -380,18 +380,31 @@ func (svr *Service) stop() {
}
}
// TODO(fatedier): Use StatusExporter to provide query interfaces instead of directly using methods from the Service.
func (svr *Service) GetProxyStatus(name string) (*proxy.WorkingStatus, error) {
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
svr.ctlMu.RLock()
ctl := svr.ctl
svr.ctlMu.RUnlock()
if ctl == nil {
return nil, fmt.Errorf("control is not running")
return nil, false
}
ws, ok := ctl.pm.GetProxyStatus(name)
if !ok {
return nil, fmt.Errorf("proxy [%s] is not found", name)
}
return ws, nil
return ctl.pm.GetProxyStatus(name)
}
func (svr *Service) StatusExporter() StatusExporter {
return &statusExporterImpl{
getProxyStatusFunc: svr.getProxyStatus,
}
}
type StatusExporter interface {
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
}
type statusExporterImpl struct {
getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
}
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
return s.getProxyStatusFunc(name)
}

View File

@ -209,6 +209,7 @@ locations = ["/", "/pic"]
# routeByHTTPUser = abc
hostHeaderRewrite = "example.com"
requestHeaders.set.x-from-where = "frp"
responseHeaders.set.foo = "bar"
healthCheck.type = "http"
# frpc will send a GET http request '/status' to local http service
# http service is alive when it return 2xx http response code

6
go.mod
View File

@ -5,10 +5,10 @@ go 1.22
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.10.0
github.com/fatedier/golib v0.4.2
github.com/fatedier/golib v0.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/yamux v0.1.1
github.com/onsi/ginkgo/v2 v2.17.1
github.com/onsi/gomega v1.32.0
@ -35,7 +35,7 @@ require (
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

12
go.sum
View File

@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/golib v0.4.2 h1:k+ZBdUFTTipnP1RHfEhGbzyShRdz/rZtFGnjpXG9D9c=
github.com/fatedier/golib v0.4.2/go.mod h1:gpu+1vXxtJ072NYaNsn/YWgojDL8Ap2kFZQtbzT2qkg=
github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs=
github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
@ -64,8 +64,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=

View File

@ -19,48 +19,55 @@ mkdir -p ./release/packages
os_all='linux windows darwin freebsd android'
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
extra_all='_ hf'
cd ./release
for os in $os_all; do
for arch in $arch_all; do
frp_dir_name="frp_${frp_version}_${os}_${arch}"
frp_path="./packages/frp_${frp_version}_${os}_${arch}"
for extra in $extra_all; do
suffix="${os}_${arch}"
if [ "x${extra}" != x"_" ]; then
suffix="${os}_${arch}_${extra}"
fi
frp_dir_name="frp_${frp_version}_${suffix}"
frp_path="./packages/frp_${frp_version}_${suffix}"
if [ "x${os}" = x"windows" ]; then
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
else
if [ ! -f "./frpc_${os}_${arch}" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch} ${frp_path}/frpc
mv ./frps_${os}_${arch} ${frp_path}/frps
fi
cp ../LICENSE ${frp_path}
cp -f ../conf/frpc.toml ${frp_path}
cp -f ../conf/frps.toml ${frp_path}
if [ "x${os}" = x"windows" ]; then
if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
continue
fi
if [ ! -f "./frps_${os}_${arch}.exe" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
else
if [ ! -f "./frpc_${suffix}" ]; then
continue
fi
if [ ! -f "./frps_${suffix}" ]; then
continue
fi
mkdir ${frp_path}
mv ./frpc_${suffix} ${frp_path}/frpc
mv ./frps_${suffix} ${frp_path}/frps
fi
cp ../LICENSE ${frp_path}
cp -f ../conf/frpc.toml ${frp_path}
cp -f ../conf/frps.toml ${frp_path}
# packages
cd ./packages
if [ "x${os}" = x"windows" ]; then
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
else
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
fi
cd ..
rm -rf ${frp_path}
# packages
cd ./packages
if [ "x${os}" = x"windows" ]; then
zip -rq ${frp_dir_name}.zip ${frp_dir_name}
else
tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
fi
cd ..
rm -rf ${frp_path}
done
done
done

View File

@ -136,8 +136,14 @@ func (c *ClientTransportConfig) Complete() {
c.PoolCount = util.EmptyOr(c.PoolCount, 1)
c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
if lo.FromPtr(c.TCPMux) {
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
} else {
c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
}
c.QUIC.Complete()
c.TLS.Complete()
}

View File

@ -291,6 +291,7 @@ type HTTPProxyConfig struct {
HTTPPassword string `json:"httpPassword,omitempty"`
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"`
ResponseHeaders HeaderOperations `json:"responseHeaders,omitempty"`
RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"`
}
@ -304,6 +305,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
m.HTTPUser = c.HTTPUser
m.HTTPPwd = c.HTTPPassword
m.Headers = c.RequestHeaders.Set
m.ResponseHeaders = c.ResponseHeaders.Set
m.RouteByHTTPUser = c.RouteByHTTPUser
}
@ -317,6 +319,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
c.HTTPUser = m.HTTPUser
c.HTTPPassword = m.HTTPPwd
c.RequestHeaders.Set = m.Headers
c.ResponseHeaders.Set = m.ResponseHeaders
c.RouteByHTTPUser = m.RouteByHTTPUser
}

View File

@ -179,7 +179,12 @@ func (c *ServerTransportConfig) Complete() {
c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
if lo.FromPtr(c.TCPMux) {
// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
} else {
c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
}
c.QUIC.Complete()
if c.TLS.TrustedCaFile != "" {
c.TLS.Force = true

View File

@ -121,6 +121,7 @@ type NewProxy struct {
HTTPPwd string `json:"http_pwd,omitempty"`
HostHeaderRewrite string `json:"host_header_rewrite,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
// stcp, sudp, xtcp

View File

@ -19,11 +19,15 @@ package plugin
import (
"crypto/tls"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"github.com/fatedier/golib/pool"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@ -67,7 +71,9 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
Transport: tr,
Transport: tr,
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
p.s = &http.Server{

View File

@ -54,7 +54,8 @@ func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) {
}
hp.s = &http.Server{
Handler: hp,
Handler: hp,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {

View File

@ -20,12 +20,17 @@ import (
"crypto/tls"
"fmt"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"time"
"github.com/fatedier/golib/pool"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@ -63,10 +68,13 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
p.s = &http.Server{
Handler: rp,
Handler: rp,
ReadHeaderTimeout: 60 * time.Second,
}
var (

View File

@ -20,12 +20,17 @@ import (
"crypto/tls"
"fmt"
"io"
stdlog "log"
"net"
"net/http"
"net/http/httputil"
"time"
"github.com/fatedier/golib/pool"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
)
@ -68,11 +73,14 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
req.Header.Set(k, v)
}
},
Transport: tr,
Transport: tr,
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
}
p.s = &http.Server{
Handler: rp,
Handler: rp,
ReadHeaderTimeout: 60 * time.Second,
}
var (

View File

@ -60,7 +60,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
sp.s = &http.Server{
Handler: router,
Handler: router,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {
_ = sp.s.Serve(listener)

View File

@ -363,11 +363,13 @@ func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration)
timer := time.NewTimer(timeout)
defer timer.Stop()
statusExporter := s.vc.Service().StatusExporter()
for {
select {
case <-ticker.C:
ps, err := s.vc.Service().GetProxyStatus(name)
if err != nil {
ps, ok := statusExporter.GetProxyStatus(name)
if !ok {
continue
}
switch ps.Phase {

View File

@ -15,11 +15,20 @@
package log
import (
"bytes"
"os"
"github.com/fatedier/golib/log"
)
var (
TraceLevel = log.TraceLevel
DebugLevel = log.DebugLevel
InfoLevel = log.InfoLevel
WarnLevel = log.WarnLevel
ErrorLevel = log.ErrorLevel
)
var Logger *log.Logger
func init() {
@ -77,3 +86,24 @@ func Debugf(format string, v ...interface{}) {
func Tracef(format string, v ...interface{}) {
Logger.Tracef(format, v...)
}
func Logf(level log.Level, offset int, format string, v ...interface{}) {
Logger.Logf(level, offset, format, v...)
}
type WriteLogger struct {
level log.Level
offset int
}
func NewWriteLogger(level log.Level, offset int) *WriteLogger {
return &WriteLogger{
level: level,
offset: offset,
}
}
func (w *WriteLogger) Write(p []byte) (n int, err error) {
Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n")))
return len(p), nil
}

View File

@ -5,11 +5,11 @@ import (
"net"
"net/url"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
"golang.org/x/net/websocket"
)
func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libdial.AfterHookFunc {
func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libnet.AfterHookFunc {
return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
if enableTLS && !disableCustomTLSHeadByte {
_, err := c.Write([]byte{byte(FRPTLSHeadByte)})
@ -21,7 +21,7 @@ func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) li
}
}
func DialHookWebsocket(protocol string, host string) libdial.AfterHookFunc {
func DialHookWebsocket(protocol string, host string) libnet.AfterHookFunc {
return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
if protocol != "wss" {
protocol = "ws"

View File

@ -4,6 +4,7 @@ import (
"errors"
"net"
"net/http"
"time"
"golang.org/x/net/websocket"
)
@ -39,8 +40,9 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
}))
wl.server = &http.Server{
Addr: ln.Addr().String(),
Handler: muxer,
Addr: ln.Addr().String(),
Handler: muxer,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {

View File

@ -14,7 +14,7 @@
package version
var version = "0.57.0"
var version = "0.58.0"
func Full() string {
return version

View File

@ -15,7 +15,6 @@
package vhost
import (
"bytes"
"context"
"encoding/base64"
"errors"
@ -64,9 +63,9 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
req := r.Out
req.URL.Scheme = "http"
reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
rc := req.Context().Value(RouteConfigKey).(*RouteConfig)
if rc != nil {
if rc.RewriteHost != "" {
req.Host = rc.RewriteHost
@ -78,7 +77,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
endpoint, _ = rc.ChooseEndpointFn()
reqRouteInfo.Endpoint = endpoint
log.Tracef("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
endpoint, originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
}
// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
req.URL.Host = rc.Domain + "." +
@ -93,6 +92,15 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
req.URL.Host = req.Host
}
},
ModifyResponse: func(r *http.Response) error {
rc := r.Request.Context().Value(RouteConfigKey).(*RouteConfig)
if rc != nil {
for k, v := range rc.ResponseHeaders {
r.Header.Set(k, v)
}
}
return nil
},
// Create a connection to one proxy routed by route policy.
Transport: &http.Transport{
ResponseHeaderTimeout: rp.responseHeaderTimeout,
@ -116,10 +124,16 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
return nil, nil
},
},
BufferPool: newWrapPool(),
ErrorLog: stdlog.New(newWrapLogger(), "", 0),
BufferPool: pool.NewBuffer(32 * 1024),
ErrorLog: stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
log.Warnf("do http proxy request [host: %s] error: %v", req.Host, err)
log.Logf(log.WarnLevel, 1, "do http proxy request [host: %s] error: %v", req.Host, err)
if err != nil {
if e, ok := err.(net.Error); ok && e.Timeout() {
rw.WriteHeader(http.StatusGatewayTimeout)
return
}
}
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write(getNotFoundPageContent())
},
@ -152,14 +166,6 @@ func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser str
return nil
}
func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
headers = vr.payload.(*RouteConfig).Headers
}
return
}
// CreateConnection create a new connection by route config
func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
host, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
@ -300,8 +306,13 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
RemoteAddr: req.RemoteAddr,
URLHost: req.URL.Host,
}
originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
rc := rp.GetRouteConfig(originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
newctx := req.Context()
newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo)
newctx = context.WithValue(newctx, RouteConfigKey, rc)
return req.Clone(newctx)
}
@ -322,20 +333,3 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
rp.proxy.ServeHTTP(rw, newreq)
}
}
type wrapPool struct{}
func newWrapPool() *wrapPool { return &wrapPool{} }
func (p *wrapPool) Get() []byte { return pool.GetBuf(32 * 1024) }
func (p *wrapPool) Put(buf []byte) { pool.PutBuf(buf) }
type wrapLogger struct{}
func newWrapLogger() *wrapLogger { return &wrapLogger{} }
func (l *wrapLogger) Write(p []byte) (n int, err error) {
log.Warnf("%s", string(bytes.TrimRight(p, "\n")))
return len(p), nil
}

View File

@ -12,7 +12,7 @@ import (
func TestGetHTTPSHostname(t *testing.T) {
require := require.New(t)
l, err := net.Listen("tcp", ":")
l, err := net.Listen("tcp", "127.0.0.1:")
require.NoError(err)
defer l.Close()

View File

@ -29,7 +29,8 @@ import (
type RouteInfo string
const (
RouteInfoKey RouteInfo = "routeInfo"
RouteInfoKey RouteInfo = "routeInfo"
RouteConfigKey RouteInfo = "routeConfig"
)
type RequestRouteInfo struct {
@ -113,6 +114,7 @@ type RouteConfig struct {
Username string
Password string
Headers map[string]string
ResponseHeaders map[string]string
RouteByHTTPUser string
CreateConnFn CreateConnFunc

View File

@ -297,20 +297,18 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
}
func (ctl *Control) heartbeatWorker() {
xl := ctl.xl
// Don't need application heartbeat if TCPMux is enabled,
// yamux will do same thing.
// TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it.
if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
go wait.Until(func() {
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warnf("heartbeat timeout")
ctl.conn.Close()
return
}
}, time.Second, ctl.doneCh)
if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 {
return
}
xl := ctl.xl
go wait.Until(func() {
if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
xl.Warnf("heartbeat timeout")
ctl.conn.Close()
return
}
}, time.Second, ctl.doneCh)
}
// block until Control closed

View File

@ -32,8 +32,6 @@ import (
"github.com/fatedier/frp/pkg/util/version"
)
// TODO(fatedier): add an API to clean status of all offline proxies.
type GeneralResponse struct {
Code int
Msg string
@ -146,7 +144,8 @@ type TCPOutConf struct {
type TCPMuxOutConf struct {
BaseOutConf
v1.DomainConfig
Multiplexer string `json:"multiplexer"`
Multiplexer string `json:"multiplexer"`
RouteByHTTPUser string `json:"routeByHTTPUser"`
}
type UDPOutConf struct {

View File

@ -58,6 +58,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
RewriteHost: pxy.cfg.HostHeaderRewrite,
RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
Headers: pxy.cfg.RequestHeaders.Set,
ResponseHeaders: pxy.cfg.ResponseHeaders.Set,
Username: pxy.cfg.HTTPUser,
Password: pxy.cfg.HTTPPassword,
CreateConnFn: pxy.GetRealConn,

View File

@ -286,12 +286,13 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
server := &http.Server{
Addr: address,
Handler: rp,
Addr: address,
Handler: rp,
ReadHeaderTimeout: 60 * time.Second,
}
var l net.Listener
if httpMuxOn {
l = svr.muxer.ListenHttp(1)
l = svr.muxer.ListenHTTP(1)
} else {
l, err = net.Listen("tcp", address)
if err != nil {
@ -308,7 +309,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
if cfg.VhostHTTPSPort > 0 {
var l net.Listener
if httpsMuxOn {
l = svr.muxer.ListenHttps(1)
l = svr.muxer.ListenHTTPS(1)
} else {
address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
l, err = net.Listen("tcp", address)

View File

@ -260,7 +260,7 @@ func (f *Framework) SetEnvs(envs []string) {
func (f *Framework) WriteTempFile(name string, content string) string {
filePath := filepath.Join(f.TempDirectory, name)
err := os.WriteFile(filePath, []byte(content), 0o766)
err := os.WriteFile(filePath, []byte(content), 0o600)
ExpectNoError(err)
return filePath
}

View File

@ -27,7 +27,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
currentServerProcesses := make([]*process.Process, 0, len(serverTemplates))
for i := range serverTemplates {
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
err = os.WriteFile(path, []byte(outs[i]), 0o666)
err = os.WriteFile(path, []byte(outs[i]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
@ -48,7 +48,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
for i := range clientTemplates {
index := i + len(serverTemplates)
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
err = os.WriteFile(path, []byte(outs[index]), 0o666)
err = os.WriteFile(path, []byte(outs[index]), 0o600)
ExpectNoError(err)
if TestContext.Debug {
@ -94,7 +94,7 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
func (f *Framework) GenerateConfigFile(content string) string {
f.configFileIndex++
path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-config-%d", f.configFileIndex))
err := os.WriteFile(path, []byte(content), 0o666)
err := os.WriteFile(path, []byte(content), 0o600)
ExpectNoError(err)
return path
}

View File

@ -12,7 +12,7 @@ import (
"strconv"
"time"
libdial "github.com/fatedier/golib/net/dial"
libnet "github.com/fatedier/golib/net"
httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/test/e2e/pkg/rpc"
@ -160,11 +160,11 @@ func (r *Request) Do() (*Response, error) {
if r.protocol != "tcp" {
return nil, fmt.Errorf("only tcp protocol is allowed for proxy")
}
proxyType, proxyAddress, auth, err := libdial.ParseProxyURL(r.proxyURL)
proxyType, proxyAddress, auth, err := libnet.ParseProxyURL(r.proxyURL)
if err != nil {
return nil, fmt.Errorf("parse ProxyURL error: %v", err)
}
conn, err = libdial.Dial(addr, libdial.WithProxy(proxyType, proxyAddress), libdial.WithProxyAuth(auth))
conn, err = libnet.Dial(addr, libnet.WithProxy(proxyType, proxyAddress), libnet.WithProxyAuth(auth))
if err != nil {
return nil, err
}

View File

@ -5,6 +5,7 @@ import (
"net/http"
"net/url"
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/onsi/ginkgo/v2"
@ -266,7 +267,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
Ensure()
})
ginkgo.It("Modify headers", func() {
ginkgo.It("Modify request headers", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
@ -291,7 +292,6 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
f.RunProcesses([]string{serverConf}, []string{clientConf})
// not set auth header
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com")
@ -300,6 +300,40 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
Ensure()
})
ginkgo.It("Modify response headers", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
localPort := f.AllocPort()
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
})),
)
f.RunServer("", localServer)
clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[[proxies]]
name = "test"
type = "http"
localPort = %d
customDomains = ["normal.example.com"]
responseHeaders.set.x-from-where = "frp"
`, localPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com")
}).
Ensure(func(res *request.Response) bool {
return res.Header.Get("X-From-Where") == "frp"
})
})
ginkgo.It("Host Header Rewrite", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
@ -385,4 +419,48 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
framework.ExpectNoError(err)
framework.ExpectEqualValues(consts.TestString, string(msg))
})
ginkgo.It("vhostHTTPTimeout", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
serverConf += `
vhostHTTPTimeout = 2
`
delayDuration := 0 * time.Second
localPort := f.AllocPort()
localServer := httpserver.New(
httpserver.WithBindPort(localPort),
httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
time.Sleep(delayDuration)
_, _ = w.Write([]byte(req.Host))
})),
)
f.RunServer("", localServer)
clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[[proxies]]
name = "test"
type = "http"
localPort = %d
customDomains = ["normal.example.com"]
`, localPort)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(time.Second)
}).
ExpectResp([]byte("normal.example.com")).
Ensure()
delayDuration = 3 * time.Second
framework.NewRequestExpect(f).Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(5 * time.Second)
}).
Ensure(framework.ExpectResponseCode(504))
})
})

View File

@ -31,6 +31,7 @@ declare module 'vue' {
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']

View File

@ -39,6 +39,7 @@
<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>

View File

@ -0,0 +1,38 @@
<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>

View File

@ -1,9 +1,9 @@
<template>
<el-form
label-position="left"
label-width="auto"
inline
class="proxy-table-expand"
v-if="proxyType === 'http' || proxyType === 'https'"
>
<el-form-item label="Name">
<span>{{ row.name }}</span>
@ -11,18 +11,6 @@
<el-form-item label="Type">
<span>{{ row.type }}</span>
</el-form-item>
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
<el-form-item label="locations">
<span>{{ row.locations }}</span>
</el-form-item>
<el-form-item label="HostRewrite">
<span>{{ row.hostHeaderRewrite }}</span>
</el-form-item>
<el-form-item label="Encryption">
<span>{{ row.encryption }}</span>
</el-form-item>
@ -35,30 +23,40 @@
<el-form-item label="Last Close">
<span>{{ row.lastCloseTime }}</span>
</el-form-item>
</el-form>
<el-form label-position="left" inline class="proxy-table-expand" v-else>
<el-form-item label="Name">
<span>{{ row.name }}</span>
</el-form-item>
<el-form-item label="Type">
<span>{{ row.type }}</span>
</el-form-item>
<el-form-item label="Addr">
<span>{{ row.addr }}</span>
</el-form-item>
<el-form-item label="Encryption">
<span>{{ row.encryption }}</span>
</el-form-item>
<el-form-item label="Compression">
<span>{{ row.compression }}</span>
</el-form-item>
<el-form-item label="Last Start">
<span>{{ row.lastStartTime }}</span>
</el-form-item>
<el-form-item label="Last Close">
<span>{{ row.lastCloseTime }}</span>
</el-form-item>
<div v-if="proxyType === 'http' || proxyType === 'https'">
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
<el-form-item label="locations">
<span>{{ row.locations }}</span>
</el-form-item>
<el-form-item label="HostRewrite">
<span>{{ row.hostHeaderRewrite }}</span>
</el-form-item>
</div>
<div v-else-if="proxyType === 'tcpmux'">
<el-form-item label="Multiplexer">
<span>{{ row.multiplexer }}</span>
</el-form-item>
<el-form-item label="RouteByHTTPUser">
<span>{{ row.routeByHTTPUser }}</span>
</el-form-item>
<el-form-item label="Domains">
<span>{{ row.customDomains }}</span>
</el-form-item>
<el-form-item label="SubDomain">
<span>{{ row.subdomain }}</span>
</el-form-item>
</div>
<div v-else>
<el-form-item label="Addr">
<span>{{ row.addr }}</span>
</el-form-item>
</div>
</el-form>
<div v-if="row.annotations && row.annotations.size > 0">

View File

@ -20,10 +20,10 @@
<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">
<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">
<el-form-item label="HTTPS Port" v-if="data.vhostHTTPSPort != 0">
<span>{{ data.vhostHTTPSPort }}</span>
</el-form-item>
<el-form-item

View File

@ -4,6 +4,7 @@ import ProxiesTCP from '../components/ProxiesTCP.vue'
import ProxiesUDP from '../components/ProxiesUDP.vue'
import ProxiesHTTP from '../components/ProxiesHTTP.vue'
import ProxiesHTTPS from '../components/ProxiesHTTPS.vue'
import ProxiesTCPMux from '../components/ProxiesTCPMux.vue'
import ProxiesSTCP from '../components/ProxiesSTCP.vue'
import ProxiesSUDP from '../components/ProxiesSUDP.vue'
@ -35,6 +36,11 @@ const router = createRouter({
name: 'ProxiesHTTPS',
component: ProxiesHTTPS,
},
{
path: '/proxies/tcpmux',
name: 'ProxiesTCPMux',
component: ProxiesTCPMux,
},
{
path: '/proxies/stcp',
name: 'ProxiesSTCP',

View File

@ -110,6 +110,28 @@ class HTTPSProxy extends BaseProxy {
}
}
class TCPMuxProxy extends BaseProxy {
multiplexer: string
routeByHTTPUser: string
constructor(proxyStats: any, port: number, subdomainHost: string) {
super(proxyStats)
this.type = 'tcpmux'
this.port = port
this.multiplexer = ''
this.routeByHTTPUser = ''
if (proxyStats.conf) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains
this.multiplexer = proxyStats.conf.multiplexer
this.routeByHTTPUser = proxyStats.conf.routeByHTTPUser
if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
}
}
}
}
class STCPProxy extends BaseProxy {
constructor(proxyStats: any) {
super(proxyStats)
@ -128,6 +150,7 @@ export {
BaseProxy,
TCPProxy,
UDPProxy,
TCPMuxProxy,
HTTPProxy,
HTTPSProxy,
STCPProxy,