diff --git a/.circleci/config.yml b/.circleci/config.yml index 854747ef..c1b35527 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: test1: docker: - - image: circleci/golang:1.15-node + - image: circleci/golang:1.16-node working_directory: /go/src/github.com/fatedier/frp steps: - checkout @@ -10,7 +10,7 @@ jobs: - run: make alltest test2: docker: - - image: circleci/golang:1.14-node + - image: circleci/golang:1.15-node working_directory: /go/src/github.com/fatedier/frp steps: - checkout diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml index 836058d9..a507ea01 100644 --- a/.github/workflows/build-and-push-image.yml +++ b/.github/workflows/build-and-push-image.yml @@ -18,7 +18,7 @@ jobs: name: Set up Go 1.x uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - run: go version - diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 7c4a403c..665d535b 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - name: Make All run: | diff --git a/.gitignore b/.gitignore index 1df3ebab..eeccf24a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ release/ test/bin/ vendor/ dist/ +.idea/ # Cache *.swp diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index 091d02ea..39821767 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -2,34 +2,24 @@ export PATH := $(GOPATH)/bin:$(PATH) export GO111MODULE=on LDFLAGS := -s -w +os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm windows:386 windows:amd64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat + all: build build: app app: - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_darwin_amd64 ./cmd/frpc - env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_darwin_amd64 ./cmd/frps - env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_386 ./cmd/frpc - env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_386 ./cmd/frps - env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_amd64 ./cmd/frpc - env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_amd64 ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_386 ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_386 ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_amd64 ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_amd64 ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm64 ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm64 ./cmd/frps - env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_386.exe ./cmd/frpc - env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_windows_386.exe ./cmd/frps - env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_amd64.exe ./cmd/frpc - env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_windows_amd64.exe ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64 ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64 ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64le ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64le ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips ./cmd/frps - env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mipsle ./cmd/frpc - env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mipsle ./cmd/frps + @$(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)" -o ./release/frpc_$${target_suffix} ./cmd/frpc;\ + env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\ + echo "Build $${os}-$${arch} done";\ + ) + @mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe + @mv ./release/frps_windows_386 ./release/frps_windows_386.exe + @mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe + @mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe diff --git a/README.md b/README.md index 14417955..670ce32c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # frp [![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/fatedier/frp) @@ -67,7 +68,7 @@ frp also has a P2P connect mode. * [Donation](#donation) * [AliPay](#alipay) * [Wechat Pay](#wechat-pay) - * [Paypal](#paypal) + * [PayPal](#paypal) @@ -257,7 +258,9 @@ Configure `frps` same as above. 2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct user and password to view files in `/tmp/files` on the `frpc` machine. -### Enable HTTPS for local HTTP service +### Enable HTTPS for local HTTP(S) service + +You may substitute `https2https` for the plugin, and point the `plugin_local_addr` to a HTTPS endpoint. 1. Start `frpc` with configuration: @@ -515,11 +518,100 @@ use_compression = true frp supports the TLS protocol between `frpc` and `frps` since v0.25.0. -Config `tls_enable = true` in the `[common]` section to `frpc.ini` to enable this feature. - For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. -To enforce `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. +Configure `tls_enable = true` in the `[common]` section to `frpc.ini` to enable this feature. + +To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.** + +**`frpc` TLS settings (under the `[common]` section):** +```ini +tls_enable = true +tls_cert_file = certificate.crt +tls_key_file = certificate.key +tls_trusted_ca_file = ca.crt +``` + +**`frps` TLS settings (under the `[common]` section):** +```ini +tls_only = true +tls_enable = true +tls_cert_file = certificate.crt +tls_key_file = certificate.key +tls_trusted_ca_file = ca.crt +``` + +You will need **a root CA cert** and **at least one SSL/TLS certificate**. It **can** be self-signed or regular (such as Let's Encrypt or another SSL/TLS certificate provider). + +If you using `frp` via IP address and not hostname, make sure to set the appropriate IP address in the Subject Alternative Name (SAN) area when generating SSL/TLS Certificates. + +Given an example: + +* Prepare openssl config file. It exists at `/etc/pki/tls/openssl.cnf` in Linux System and `/System/Library/OpenSSL/openssl.cnf` in MacOS, and you can copy it to current path, like `cp /etc/pki/tls/openssl.cnf ./my-openssl.cnf`. If not, you can build it by yourself, like: +``` +cat > my-openssl.cnf << EOF +[ ca ] +default_ca = CA_default +[ CA_default ] +x509_extensions = usr_cert +[ req ] +default_bits = 2048 +default_md = sha256 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca +string_mask = utf8only +[ req_distinguished_name ] +[ req_attributes ] +[ usr_cert ] +basicConstraints = CA:FALSE +nsComment = "OpenSSL Generated Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +[ v3_ca ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = CA:true +EOF +``` + +* build ca certificates: +``` +openssl genrsa -out ca.key 2048 +openssl req -x509 -new -nodes -key ca.key -subj "/CN=example.ca.com" -days 5000 -out ca.crt +``` + +* build frps certificates: +``` +openssl genrsa -out server.key 2048 + +openssl req -new -sha256 -key server.key \ + -subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=server.com" \ + -reqexts SAN \ + -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com")) \ + -out server.csr + +openssl x509 -req -days 365 \ + -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \ + -out server.crt +``` + +* build frpc certificates: +``` +openssl genrsa -out client.key 2048 +openssl req -new -sha256 -key client.key \ + -subj "/C=XX/ST=DEFAULT/L=DEFAULT/O=DEFAULT/CN=client.com" \ + -reqexts SAN \ + -config <(cat my-openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:client.com,DNS:example.client.com")) \ + -out client.csr + +openssl x509 -req -days 365 \ + -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -extfile <(printf "subjectAltName=DNS:client.com,DNS:example.client.com") \ + -out client.crt +``` ### Hot-Reloading frpc configuration @@ -967,6 +1059,6 @@ frp QQ group: 606194980 ![donation-wechatpay](/doc/pic/donate-wechatpay.png) -### Paypal +### PayPal -Donate money by [paypal](https://www.paypal.me/fatedier) to my account **fatedier@gmail.com**. +Donate money by [PayPal](https://www.paypal.me/fatedier) to my account **fatedier@gmail.com**. diff --git a/Release.md b/Release.md index e792ed8a..658d4a33 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,12 @@ +### New + +* New plugin `https2https`. +* frpc supports `tls_server_name` to override the default value from `server_addr`. + +### Improvement + +* Increase reconnect frequency if it occurs an network error between frpc and frps. + ### Fix -* Reduce binary file size. +* Fix panic issue about xtcp. diff --git a/client/admin.go b/client/admin.go index fdef060f..b3485af2 100644 --- a/client/admin.go +++ b/client/admin.go @@ -15,7 +15,6 @@ package client import ( - "fmt" "net" "net/http" "time" @@ -31,7 +30,7 @@ var ( httpServerWriteTimeout = 10 * time.Second ) -func (svr *Service) RunAdminServer(addr string, port int) (err error) { +func (svr *Service) RunAdminServer(address string) (err error) { // url router router := mux.NewRouter() @@ -51,7 +50,6 @@ func (svr *Service) RunAdminServer(addr string, port int) (err error) { http.Redirect(w, r, "/static/", http.StatusMovedPermanently) }) - address := fmt.Sprintf("%s:%d", addr, port) server := &http.Server{ Addr: address, Handler: router, diff --git a/client/admin_api.go b/client/admin_api.go index 3977df1f..c5548fb5 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -62,7 +62,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) { return } - pxyCfgs, visitorCfgs, err := config.LoadAllConfFromIni(svr.cfg.User, content, newCommonCfg.Start) + pxyCfgs, visitorCfgs, err := config.LoadAllProxyConfsFromIni(svr.cfg.User, content, newCommonCfg.Start) if err != nil { res.Code = 400 res.Msg = err.Error() @@ -243,7 +243,7 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) { return } - rows := strings.Split(content, "\n") + rows := strings.Split(string(content), "\n") newRows := make([]string, 0, len(rows)) for _, row := range rows { row = strings.TrimSpace(row) diff --git a/client/control.go b/client/control.go index 4d16ce99..42cc0464 100644 --- a/client/control.go +++ b/client/control.go @@ -209,13 +209,17 @@ func (ctl *Control) connectServer() (conn net.Conn, err error) { conn = stream } else { var tlsConfig *tls.Config + sn := ctl.clientCfg.TLSServerName + if sn == "" { + sn = ctl.clientCfg.ServerAddr + } if ctl.clientCfg.TLSEnable { tlsConfig, err = transport.NewClientTLSConfig( ctl.clientCfg.TLSCertFile, ctl.clientCfg.TLSKeyFile, ctl.clientCfg.TLSTrustedCaFile, - ctl.clientCfg.ServerAddr) + sn) if err != nil { xl.Warn("fail to build tls configuration when connecting to server, err: %v", err) diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 3e97784a..f208083c 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -148,7 +148,7 @@ func (pxy *TCPProxy) Close() { } func (pxy *TCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, conn, []byte(pxy.clientCfg.Token), m) } @@ -177,7 +177,7 @@ func (pxy *TCPMuxProxy) Close() { } func (pxy *TCPMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, conn, []byte(pxy.clientCfg.Token), m) } @@ -206,7 +206,7 @@ func (pxy *HTTPProxy) Close() { } func (pxy *HTTPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, conn, []byte(pxy.clientCfg.Token), m) } @@ -235,7 +235,7 @@ func (pxy *HTTPSProxy) Close() { } func (pxy *HTTPSProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, conn, []byte(pxy.clientCfg.Token), m) } @@ -264,7 +264,7 @@ func (pxy *STCPProxy) Close() { } func (pxy *STCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, conn, []byte(pxy.clientCfg.Token), m) } @@ -309,6 +309,10 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { raddr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", pxy.clientCfg.ServerAddr, pxy.serverUDPPort)) clientConn, err := net.DialUDP("udp", nil, raddr) + if err != nil { + xl.Error("dial server udp addr error: %v", err) + return + } defer clientConn.Close() err = msg.WriteMsg(clientConn, natHoleClientMsg) @@ -410,7 +414,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { return } - HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter, muxConn, []byte(pxy.cfg.Sk), m) } diff --git a/client/service.go b/client/service.go index 2b1b38aa..3033ee2a 100644 --- a/client/service.go +++ b/client/service.go @@ -17,6 +17,7 @@ package client import ( "context" "crypto/tls" + "errors" "fmt" "io/ioutil" "net" @@ -128,7 +129,8 @@ func (svr *Service) Run() error { return fmt.Errorf("Load assets error: %v", err) } - err = svr.RunAdminServer(svr.cfg.AdminAddr, svr.cfg.AdminPort) + address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort)) + err = svr.RunAdminServer(address) if err != nil { log.Warn("run admin server error: %v", err) } @@ -177,9 +179,16 @@ func (svr *Service) keepControllerWorking() { if err != nil { xl.Warn("reconnect to server error: %v", err) time.Sleep(delayTime) - delayTime = delayTime * 2 - if delayTime > maxDelayTime { - delayTime = maxDelayTime + + opErr := &net.OpError{} + // quick retry for dial error + if errors.As(err, &opErr) && opErr.Op == "dial" { + delayTime = 2 * time.Second + } else { + delayTime = delayTime * 2 + if delayTime > maxDelayTime { + delayTime = maxDelayTime + } } continue } @@ -206,11 +215,16 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) { xl := xlog.FromContextSafe(svr.ctx) var tlsConfig *tls.Config if svr.cfg.TLSEnable { + sn := svr.cfg.TLSServerName + if sn == "" { + sn = svr.cfg.ServerAddr + } + tlsConfig, err = transport.NewClientTLSConfig( svr.cfg.TLSCertFile, svr.cfg.TLSKeyFile, svr.cfg.TLSTrustedCaFile, - svr.cfg.ServerAddr) + sn) if err != nil { xl.Warn("fail to build tls configuration when service login, err: %v", err) return diff --git a/cmd/frpc/sub/http.go b/cmd/frpc/sub/http.go index d1286b2e..03593f1a 100644 --- a/cmd/frpc/sub/http.go +++ b/cmd/frpc/sub/http.go @@ -47,7 +47,7 @@ var httpCmd = &cobra.Command{ Use: "http", Short: "Run frpc with a single http proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/https.go b/cmd/frpc/sub/https.go index 99a22953..d636f426 100644 --- a/cmd/frpc/sub/https.go +++ b/cmd/frpc/sub/https.go @@ -43,7 +43,7 @@ var httpsCmd = &cobra.Command{ Use: "https", Short: "Run frpc with a single https proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index a085dbc4..bf867932 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -129,9 +129,9 @@ func handleSignal(svr *client.Service) { close(kcpDoneCh) } -func parseClientCommonCfg(fileType int, content string) (cfg config.ClientCommonConf, err error) { +func parseClientCommonCfg(fileType int, source []byte) (cfg config.ClientCommonConf, err error) { if fileType == CfgFileTypeIni { - cfg, err = parseClientCommonCfgFromIni(content) + cfg, err = config.UnmarshalClientConfFromIni(source) } else if fileType == CfgFileTypeCmd { cfg, err = parseClientCommonCfgFromCmd() } @@ -146,14 +146,6 @@ func parseClientCommonCfg(fileType int, content string) (cfg config.ClientCommon return } -func parseClientCommonCfgFromIni(content string) (config.ClientCommonConf, error) { - cfg, err := config.UnmarshalClientConfFromIni(content) - if err != nil { - return config.ClientCommonConf{}, err - } - return cfg, err -} - func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { cfg = config.GetDefaultClientConf() @@ -191,7 +183,7 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { } func runClient(cfgFilePath string) (err error) { - var content string + var content []byte content, err = config.GetRenderedConfFromFile(cfgFilePath) if err != nil { return @@ -202,9 +194,9 @@ func runClient(cfgFilePath string) (err error) { return } - pxyCfgs, visitorCfgs, err := config.LoadAllConfFromIni(cfg.User, content, cfg.Start) + pxyCfgs, visitorCfgs, err := config.LoadAllProxyConfsFromIni(cfg.User, content, cfg.Start) if err != nil { - return err + return } err = startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) diff --git a/cmd/frpc/sub/stcp.go b/cmd/frpc/sub/stcp.go index 1b4ac0f1..673a268e 100644 --- a/cmd/frpc/sub/stcp.go +++ b/cmd/frpc/sub/stcp.go @@ -45,7 +45,7 @@ var stcpCmd = &cobra.Command{ Use: "stcp", Short: "Run frpc with a single stcp proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/sudp.go b/cmd/frpc/sub/sudp.go index d7306771..3c3d5a8b 100644 --- a/cmd/frpc/sub/sudp.go +++ b/cmd/frpc/sub/sudp.go @@ -45,7 +45,7 @@ var sudpCmd = &cobra.Command{ Use: "sudp", Short: "Run frpc with a single sudp proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/tcp.go b/cmd/frpc/sub/tcp.go index e6f310fd..b62cb74a 100644 --- a/cmd/frpc/sub/tcp.go +++ b/cmd/frpc/sub/tcp.go @@ -41,7 +41,7 @@ var tcpCmd = &cobra.Command{ Use: "tcp", Short: "Run frpc with a single tcp proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go index 065bf9c1..6f46cf76 100644 --- a/cmd/frpc/sub/tcpmux.go +++ b/cmd/frpc/sub/tcpmux.go @@ -44,7 +44,7 @@ var tcpMuxCmd = &cobra.Command{ Use: "tcpmux", Short: "Run frpc with a single tcpmux proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/udp.go b/cmd/frpc/sub/udp.go index c5219960..7f6dd3f0 100644 --- a/cmd/frpc/sub/udp.go +++ b/cmd/frpc/sub/udp.go @@ -41,7 +41,7 @@ var udpCmd = &cobra.Command{ Use: "udp", Short: "Run frpc with a single udp proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/xtcp.go b/cmd/frpc/sub/xtcp.go index 6e66f953..1eb096f7 100644 --- a/cmd/frpc/sub/xtcp.go +++ b/cmd/frpc/sub/xtcp.go @@ -45,7 +45,7 @@ var xtcpCmd = &cobra.Command{ Use: "xtcp", Short: "Run frpc with a single xtcp proxy", RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frps/root.go b/cmd/frps/root.go index 6dd268f3..b76817bc 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -106,7 +106,7 @@ var rootCmd = &cobra.Command{ var err error if cfgFile != "" { log.Info("frps uses config file: %s", cfgFile) - var content string + var content []byte content, err = config.GetRenderedConfFromFile(cfgFile) if err != nil { return err @@ -114,7 +114,7 @@ var rootCmd = &cobra.Command{ cfg, err = parseServerCommonCfg(CfgFileTypeIni, content) } else { log.Info("frps uses command line arguments for config") - cfg, err = parseServerCommonCfg(CfgFileTypeCmd, "") + cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil) } if err != nil { return err @@ -135,9 +135,9 @@ func Execute() { } } -func parseServerCommonCfg(fileType int, content string) (cfg config.ServerCommonConf, err error) { +func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) { if fileType == CfgFileTypeIni { - cfg, err = parseServerCommonCfgFromIni(content) + cfg, err = config.UnmarshalServerConfFromIni(source) } else if fileType == CfgFileTypeCmd { cfg, err = parseServerCommonCfgFromCmd() } @@ -152,14 +152,6 @@ func parseServerCommonCfg(fileType int, content string) (cfg config.ServerCommon return } -func parseServerCommonCfgFromIni(content string) (config.ServerCommonConf, error) { - cfg, err := config.UnmarshalServerConfFromIni(content) - if err != nil { - return config.ServerCommonConf{}, err - } - return cfg, nil -} - func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) { cfg = config.GetDefaultServerConf() diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 1b5c700e..be299b10 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -2,6 +2,7 @@ [common] # A literal address or host name for IPv6 must be enclosed # in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single "server_addr" field, no need square brackets, like "server_addr = ::". server_addr = 0.0.0.0 server_port = 7000 @@ -78,6 +79,7 @@ tls_enable = true # tls_cert_file = client.crt # tls_key_file = client.key # tls_trusted_ca_file = ca.crt +# tls_server_name = example.com # specify a dns server, so frpc will use this instead of default one # dns_server = 8.8.8.8 @@ -246,6 +248,16 @@ plugin_key_path = ./server.key plugin_host_header_rewrite = 127.0.0.1 plugin_header_X-From-Where = frp +[plugin_https2https] +type = https +custom_domains = test.yourdomain.com +plugin = https2https +plugin_local_addr = 127.0.0.1:443 +plugin_crt_path = ./server.crt +plugin_key_path = ./server.key +plugin_host_header_rewrite = 127.0.0.1 +plugin_header_X-From-Where = frp + [plugin_http2https] type = http custom_domains = test.yourdomain.com diff --git a/conf/frps_full.ini b/conf/frps_full.ini index 2f00f5c3..a704cf4f 100644 --- a/conf/frps_full.ini +++ b/conf/frps_full.ini @@ -2,6 +2,7 @@ [common] # A literal address or host name for IPv6 must be enclosed # in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single "bind_addr" field, no need square brackets, like "bind_addr = ::". bind_addr = 0.0.0.0 bind_port = 7000 diff --git a/go.mod b/go.mod index 36ae6d69..53b24d65 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fatedier/frp -go 1.15 +go 1.16 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 @@ -23,17 +23,18 @@ require ( github.com/prometheus/client_golang v1.4.1 github.com/rakyll/statik v0.1.1 github.com/rodaine/table v1.0.0 + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/cobra v0.0.3 github.com/stretchr/testify v1.4.0 github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047 // indirect github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554 // indirect github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8 // indirect - github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + gopkg.in/ini.v1 v1.62.0 gopkg.in/square/go-jose.v2 v2.4.1 // indirect k8s.io/apimachinery v0.18.3 ) diff --git a/go.sum b/go.sum index cd561f6f..34efc8da 100644 --- a/go.sum +++ b/go.sum @@ -51,7 +51,6 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -72,6 +71,8 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= @@ -85,6 +86,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -154,6 +157,10 @@ github.com/rodaine/table v1.0.0 h1:UaCJG5Axc/cNXVGXqnCrffm1KxP0OfYLe1HuJLf5sFY= github.com/rodaine/table v1.0.0/go.mod h1:YAUzwPOji0DUJNEvggdxyQcUAl4g3hDRcFlyjnnR51I= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -171,8 +178,6 @@ github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554 h1:pexgSe+JCFuxG+uoM github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8 h1:6CNSDqI1wiE+JqyOy5Qt/yo/DoNI2/QmmOZeiCid2Nw= github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= -github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks= -github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -185,6 +190,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= @@ -205,10 +211,8 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -221,6 +225,7 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= @@ -239,6 +244,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -246,7 +253,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/hack/run-e2e.sh b/hack/run-e2e.sh index 68db6bfa..0932b2ce 100755 --- a/hack/run-e2e.sh +++ b/hack/run-e2e.sh @@ -5,7 +5,7 @@ ROOT=$(unset CDPATH && cd $(dirname "${BASH_SOURCE[0]}")/.. && pwd) which ginkgo &> /dev/null if [ $? -ne 0 ]; then echo "ginkgo not found, try to install..." - go get -u github.com/onsi/ginkgo/ginkgo + go install github.com/onsi/ginkgo/ginkgo fi debug=false diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index a77123fb..894da247 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -19,101 +19,58 @@ import ( "github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/msg" - - "github.com/vaughan0/go-ini" ) -type baseConfig struct { +type BaseConfig struct { // AuthenticationMethod specifies what authentication method to use to // authenticate frpc with frps. If "token" is specified - token will be // read into login message. If "oidc" is specified - OIDC (Open ID Connect) // token will be issued using OIDC settings. By default, this value is "token". - AuthenticationMethod string `json:"authentication_method"` + AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"` // AuthenticateHeartBeats specifies whether to include authentication token in // heartbeats sent to frps. By default, this value is false. - AuthenticateHeartBeats bool `json:"authenticate_heartbeats"` + AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"` // AuthenticateNewWorkConns specifies whether to include authentication token in // new work connections sent to frps. By default, this value is false. - AuthenticateNewWorkConns bool `json:"authenticate_new_work_conns"` + AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"` } -func getDefaultBaseConf() baseConfig { - return baseConfig{ +func getDefaultBaseConf() BaseConfig { + return BaseConfig{ AuthenticationMethod: "token", AuthenticateHeartBeats: false, AuthenticateNewWorkConns: false, } } -func unmarshalBaseConfFromIni(conf ini.File) baseConfig { - var ( - tmpStr string - ok bool - ) - - cfg := getDefaultBaseConf() - - if tmpStr, ok = conf.Get("common", "authentication_method"); ok { - cfg.AuthenticationMethod = tmpStr - } - - if tmpStr, ok = conf.Get("common", "authenticate_heartbeats"); ok && tmpStr == "true" { - cfg.AuthenticateHeartBeats = true - } else { - cfg.AuthenticateHeartBeats = false - } - - if tmpStr, ok = conf.Get("common", "authenticate_new_work_conns"); ok && tmpStr == "true" { - cfg.AuthenticateNewWorkConns = true - } else { - cfg.AuthenticateNewWorkConns = false - } - - return cfg -} - type ClientConfig struct { - baseConfig - oidcClientConfig - tokenConfig + BaseConfig `ini:",extends"` + OidcClientConfig `ini:",extends"` + TokenConfig `ini:",extends"` } func GetDefaultClientConf() ClientConfig { return ClientConfig{ - baseConfig: getDefaultBaseConf(), - oidcClientConfig: getDefaultOidcClientConf(), - tokenConfig: getDefaultTokenConf(), + BaseConfig: getDefaultBaseConf(), + OidcClientConfig: getDefaultOidcClientConf(), + TokenConfig: getDefaultTokenConf(), } } -func UnmarshalClientConfFromIni(conf ini.File) (cfg ClientConfig) { - cfg.baseConfig = unmarshalBaseConfFromIni(conf) - cfg.oidcClientConfig = unmarshalOidcClientConfFromIni(conf) - cfg.tokenConfig = unmarshalTokenConfFromIni(conf) - return cfg -} - type ServerConfig struct { - baseConfig - oidcServerConfig - tokenConfig + BaseConfig `ini:",extends"` + OidcServerConfig `ini:",extends"` + TokenConfig `ini:",extends"` } func GetDefaultServerConf() ServerConfig { return ServerConfig{ - baseConfig: getDefaultBaseConf(), - oidcServerConfig: getDefaultOidcServerConf(), - tokenConfig: getDefaultTokenConf(), + BaseConfig: getDefaultBaseConf(), + OidcServerConfig: getDefaultOidcServerConf(), + TokenConfig: getDefaultTokenConf(), } } -func UnmarshalServerConfFromIni(conf ini.File) (cfg ServerConfig) { - cfg.baseConfig = unmarshalBaseConfFromIni(conf) - cfg.oidcServerConfig = unmarshalOidcServerConfFromIni(conf) - cfg.tokenConfig = unmarshalTokenConfFromIni(conf) - return cfg -} - type Setter interface { SetLogin(*msg.Login) error SetPing(*msg.Ping) error @@ -123,9 +80,9 @@ type Setter interface { func NewAuthSetter(cfg ClientConfig) (authProvider Setter) { switch cfg.AuthenticationMethod { case consts.TokenAuthMethod: - authProvider = NewTokenAuth(cfg.baseConfig, cfg.tokenConfig) + authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) case consts.OidcAuthMethod: - authProvider = NewOidcAuthSetter(cfg.baseConfig, cfg.oidcClientConfig) + authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig) default: panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod)) } @@ -142,9 +99,9 @@ type Verifier interface { func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) { switch cfg.AuthenticationMethod { case consts.TokenAuthMethod: - authVerifier = NewTokenAuth(cfg.baseConfig, cfg.tokenConfig) + authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) case consts.OidcAuthMethod: - authVerifier = NewOidcAuthVerifier(cfg.baseConfig, cfg.oidcServerConfig) + authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig) } return authVerifier diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index a1e791aa..981f7589 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -21,30 +21,29 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/coreos/go-oidc" - "github.com/vaughan0/go-ini" "golang.org/x/oauth2/clientcredentials" ) -type oidcClientConfig struct { +type OidcClientConfig struct { // OidcClientID specifies the client ID to use to get a token in OIDC // authentication if AuthenticationMethod == "oidc". By default, this value // is "". - OidcClientID string `json:"oidc_client_id"` + OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"` // OidcClientSecret specifies the client secret to use to get a token in OIDC // authentication if AuthenticationMethod == "oidc". By default, this value // is "". - OidcClientSecret string `json:"oidc_client_secret"` + OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` // OidcAudience specifies the audience of the token in OIDC authentication //if AuthenticationMethod == "oidc". By default, this value is "". - OidcAudience string `json:"oidc_audience"` + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. // It will be used to get an OIDC token if AuthenticationMethod == "oidc". // By default, this value is "". - OidcTokenEndpointURL string `json:"oidc_token_endpoint_url"` + OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` } -func getDefaultOidcClientConf() oidcClientConfig { - return oidcClientConfig{ +func getDefaultOidcClientConf() OidcClientConfig { + return OidcClientConfig{ OidcClientID: "", OidcClientSecret: "", OidcAudience: "", @@ -52,56 +51,29 @@ func getDefaultOidcClientConf() oidcClientConfig { } } -func unmarshalOidcClientConfFromIni(conf ini.File) oidcClientConfig { - var ( - tmpStr string - ok bool - ) - - cfg := getDefaultOidcClientConf() - - if tmpStr, ok = conf.Get("common", "oidc_client_id"); ok { - cfg.OidcClientID = tmpStr - } - - if tmpStr, ok = conf.Get("common", "oidc_client_secret"); ok { - cfg.OidcClientSecret = tmpStr - } - - if tmpStr, ok = conf.Get("common", "oidc_audience"); ok { - cfg.OidcAudience = tmpStr - } - - if tmpStr, ok = conf.Get("common", "oidc_token_endpoint_url"); ok { - cfg.OidcTokenEndpointURL = tmpStr - } - - return cfg -} - -type oidcServerConfig struct { +type OidcServerConfig struct { // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer // will be used to load public keys to verify signature and will be compared // with the issuer claim in the OIDC token. It will be used if // AuthenticationMethod == "oidc". By default, this value is "". - OidcIssuer string `json:"oidc_issuer"` + OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"` // OidcAudience specifies the audience OIDC tokens should contain when validated. // If this value is empty, audience ("client ID") verification will be skipped. // It will be used when AuthenticationMethod == "oidc". By default, this // value is "". - OidcAudience string `json:"oidc_audience"` + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is // expired. It will be used when AuthenticationMethod == "oidc". By default, this // value is false. - OidcSkipExpiryCheck bool `json:"oidc_skip_expiry_check"` + OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"` // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's // issuer claim matches the issuer specified in OidcIssuer. It will be used when // AuthenticationMethod == "oidc". By default, this value is false. - OidcSkipIssuerCheck bool `json:"oidc_skip_issuer_check"` + OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"` } -func getDefaultOidcServerConf() oidcServerConfig { - return oidcServerConfig{ +func getDefaultOidcServerConf() OidcServerConfig { + return OidcServerConfig{ OidcIssuer: "", OidcAudience: "", OidcSkipExpiryCheck: false, @@ -109,44 +81,13 @@ func getDefaultOidcServerConf() oidcServerConfig { } } -func unmarshalOidcServerConfFromIni(conf ini.File) oidcServerConfig { - var ( - tmpStr string - ok bool - ) - - cfg := getDefaultOidcServerConf() - - if tmpStr, ok = conf.Get("common", "oidc_issuer"); ok { - cfg.OidcIssuer = tmpStr - } - - if tmpStr, ok = conf.Get("common", "oidc_audience"); ok { - cfg.OidcAudience = tmpStr - } - - if tmpStr, ok = conf.Get("common", "oidc_skip_expiry_check"); ok && tmpStr == "true" { - cfg.OidcSkipExpiryCheck = true - } else { - cfg.OidcSkipExpiryCheck = false - } - - if tmpStr, ok = conf.Get("common", "oidc_skip_issuer_check"); ok && tmpStr == "true" { - cfg.OidcSkipIssuerCheck = true - } else { - cfg.OidcSkipIssuerCheck = false - } - - return cfg -} - type OidcAuthProvider struct { - baseConfig + BaseConfig tokenGenerator *clientcredentials.Config } -func NewOidcAuthSetter(baseCfg baseConfig, cfg oidcClientConfig) *OidcAuthProvider { +func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider { tokenGenerator := &clientcredentials.Config{ ClientID: cfg.OidcClientID, ClientSecret: cfg.OidcClientSecret, @@ -155,7 +96,7 @@ func NewOidcAuthSetter(baseCfg baseConfig, cfg oidcClientConfig) *OidcAuthProvid } return &OidcAuthProvider{ - baseConfig: baseCfg, + BaseConfig: baseCfg, tokenGenerator: tokenGenerator, } } @@ -192,13 +133,13 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e } type OidcAuthConsumer struct { - baseConfig + BaseConfig verifier *oidc.IDTokenVerifier subjectFromLogin string } -func NewOidcAuthVerifier(baseCfg baseConfig, cfg oidcServerConfig) *OidcAuthConsumer { +func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer { provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer) if err != nil { panic(err) @@ -210,7 +151,7 @@ func NewOidcAuthVerifier(baseCfg baseConfig, cfg oidcServerConfig) *OidcAuthCons SkipIssuerCheck: cfg.OidcSkipIssuerCheck, } return &OidcAuthConsumer{ - baseConfig: baseCfg, + BaseConfig: baseCfg, verifier: provider.Verifier(&verifierConf), } } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 02faf2f9..1049174d 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -20,47 +20,30 @@ import ( "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/util/util" - - "github.com/vaughan0/go-ini" ) -type tokenConfig struct { +type TokenConfig struct { // Token specifies the authorization token used to create keys to be sent // to the server. The server must have a matching token for authorization // to succeed. By default, this value is "". - Token string `json:"token"` + Token string `ini:"token" json:"token"` } -func getDefaultTokenConf() tokenConfig { - return tokenConfig{ +func getDefaultTokenConf() TokenConfig { + return TokenConfig{ Token: "", } } -func unmarshalTokenConfFromIni(conf ini.File) tokenConfig { - var ( - tmpStr string - ok bool - ) - - cfg := getDefaultTokenConf() - - if tmpStr, ok = conf.Get("common", "token"); ok { - cfg.Token = tmpStr - } - - return cfg -} - type TokenAuthSetterVerifier struct { - baseConfig + BaseConfig token string } -func NewTokenAuth(baseCfg baseConfig, cfg tokenConfig) *TokenAuthSetterVerifier { +func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier { return &TokenAuthSetterVerifier{ - baseConfig: baseCfg, + BaseConfig: baseCfg, token: cfg.Token, } } diff --git a/pkg/config/README.md b/pkg/config/README.md new file mode 100644 index 00000000..d74f756e --- /dev/null +++ b/pkg/config/README.md @@ -0,0 +1,12 @@ +So far, there is no mature Go project that does well in parsing `*.ini` files. + +By comparison, we have selected an open source project: `https://github.com/go-ini/ini`. + +This library helped us solve most of the key-value matching, but there are still some problems, such as not supporting parsing `map`. + +We add our own logic on the basis of this library. In the current situationwhich, we need to complete the entire `Unmarshal` in two steps: + +* Step#1, use `go-ini` to complete the basic parameter matching; +* Step#2, parse our custom parameters to realize parsing special structure, like `map`, `array`. + +Some of the keywords in `tag`(like inline, extends, etc.) may be different from standard libraries such as `json` and `protobuf` in Go. For details, please refer to the library documentation: https://ini.unknwon.io/docs/intro. \ No newline at end of file diff --git a/pkg/config/client_common.go b/pkg/config/client.go similarity index 50% rename from pkg/config/client_common.go rename to pkg/config/client.go index 97f200a6..ed47e316 100644 --- a/pkg/config/client_common.go +++ b/pkg/config/client.go @@ -1,4 +1,4 @@ -// Copyright 2016 fatedier, fatedier@gmail.com +// Copyright 2020 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. @@ -17,125 +17,131 @@ package config import ( "fmt" "os" - "strconv" "strings" "github.com/fatedier/frp/pkg/auth" + "github.com/fatedier/frp/pkg/util/util" - ini "github.com/vaughan0/go-ini" + "gopkg.in/ini.v1" ) // ClientCommonConf contains information for a client service. It is // recommended to use GetDefaultClientConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ClientCommonConf struct { - auth.ClientConfig + auth.ClientConfig `ini:",extends" json:"inline"` + // ServerAddr specifies the address of the server to connect to. By // default, this value is "0.0.0.0". - ServerAddr string `json:"server_addr"` + ServerAddr string `ini:"server_addr" josn:"server_addr"` // ServerPort specifies the port to connect to the server on. By default, // this value is 7000. - ServerPort int `json:"server_port"` + ServerPort int `ini:"server_port" json:"server_port"` // HTTPProxy specifies a proxy address to connect to the server through. If // this value is "", the server will be connected to directly. By default, // this value is read from the "http_proxy" environment variable. - HTTPProxy string `json:"http_proxy"` + HTTPProxy string `ini:"http_proxy" json:"http_proxy"` // LogFile specifies a file where logs will be written to. This value will // only be used if LogWay is set appropriately. By default, this value is // "console". - LogFile string `json:"log_file"` + LogFile string `ini:"log_file" json:"log_file"` // LogWay specifies the way logging is managed. Valid values are "console" // or "file". If "console" is used, logs will be printed to stdout. If // "file" is used, logs will be printed to LogFile. By default, this value // is "console". - LogWay string `json:"log_way"` + LogWay string `ini:"log_way" json:"log_way"` // LogLevel specifies the minimum log level. Valid values are "trace", // "debug", "info", "warn", and "error". By default, this value is "info". - LogLevel string `json:"log_level"` + LogLevel string `ini:"log_level" json:"log_level"` // LogMaxDays specifies the maximum number of days to store log information // before deletion. This is only used if LogWay == "file". By default, this // value is 0. - LogMaxDays int64 `json:"log_max_days"` + LogMaxDays int64 `ini:"log_max_days" json:"log_max_days"` // DisableLogColor disables log colors when LogWay == "console" when set to // true. By default, this value is false. - DisableLogColor bool `json:"disable_log_color"` + DisableLogColor bool `ini:"disable_log_color" json:"disable_log_color"` // AdminAddr specifies the address that the admin server binds to. By // default, this value is "127.0.0.1". - AdminAddr string `json:"admin_addr"` + AdminAddr string `ini:"admin_addr" json:"admin_addr"` // AdminPort specifies the port for the admin server to listen on. If this // value is 0, the admin server will not be started. By default, this value // is 0. - AdminPort int `json:"admin_port"` + AdminPort int `ini:"admin_port" json:"admin_port"` // AdminUser specifies the username that the admin server will use for // login. By default, this value is "admin". - AdminUser string `json:"admin_user"` + AdminUser string `ini:"admin_user" json:"admin_user"` // AdminPwd specifies the password that the admin server will use for // login. By default, this value is "admin". - AdminPwd string `json:"admin_pwd"` + AdminPwd string `ini:"admin_pwd" json:"admin_pwd"` // AssetsDir specifies the local directory that the admin server will load // resources from. If this value is "", assets will be loaded from the // bundled executable using statik. By default, this value is "". - AssetsDir string `json:"assets_dir"` + AssetsDir string `ini:"assets_dir" json:"assets_dir"` // PoolCount specifies the number of connections the client will make to // the server in advance. By default, this value is 0. - PoolCount int `json:"pool_count"` + PoolCount int `ini:"pool_count" json:"pool_count"` // TCPMux toggles TCP stream multiplexing. This allows multiple requests // from a client to share a single TCP connection. If this value is true, // the server must have TCP multiplexing enabled as well. By default, this // value is true. - TCPMux bool `json:"tcp_mux"` + TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` // User specifies a prefix for proxy names to distinguish them from other // clients. If this value is not "", proxy names will automatically be // changed to "{user}.{proxy_name}". By default, this value is "". - User string `json:"user"` + User string `ini:"user" json:"user"` // DNSServer specifies a DNS server address for FRPC to use. If this value // is "", the default DNS will be used. By default, this value is "". - DNSServer string `json:"dns_server"` + DNSServer string `ini:"dns_server" json:"dns_server"` // LoginFailExit controls whether or not the client should exit after a // failed login attempt. If false, the client will retry until a login // attempt succeeds. By default, this value is true. - LoginFailExit bool `json:"login_fail_exit"` + LoginFailExit bool `ini:"login_fail_exit" json:"login_fail_exit"` // Start specifies a set of enabled proxies by name. If this set is empty, // all supplied proxies are enabled. By default, this value is an empty // set. - Start map[string]struct{} `json:"start"` + Start []string `ini:"start" json:"start"` + //Start map[string]struct{} `json:"start"` // Protocol specifies the protocol to use when interacting with the server. // Valid values are "tcp", "kcp" and "websocket". By default, this value // is "tcp". - Protocol string `json:"protocol"` + Protocol string `ini:"protocol" json:"protocol"` // TLSEnable specifies whether or not TLS should be used when communicating // with the server. If "tls_cert_file" and "tls_key_file" are valid, // client will load the supplied tls configuration. - TLSEnable bool `json:"tls_enable"` - // ClientTLSCertPath specifies the path of the cert file that client will + TLSEnable bool `ini:"tls_enable" json:"tls_enable"` + // TLSCertPath specifies the path of the cert file that client will // load. It only works when "tls_enable" is true and "tls_key_file" is valid. - TLSCertFile string `json:"tls_cert_file"` - // ClientTLSKeyPath specifies the path of the secret key file that client + TLSCertFile string `ini:"tls_cert_file" json:"tls_cert_file"` + // TLSKeyPath specifies the path of the secret key file that client // will load. It only works when "tls_enable" is true and "tls_cert_file" // are valid. - TLSKeyFile string `json:"tls_key_file"` - // TrustedCaFile specifies the path of the trusted ca file that will load. + TLSKeyFile string `ini:"tls_key_file" json:"tls_key_file"` + // TLSTrustedCaFile specifies the path of the trusted ca file that will load. // It only works when "tls_enable" is valid and tls configuration of server // has been specified. - TLSTrustedCaFile string `json:"tls_trusted_ca_file"` + TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"` + // TLSServerName specifices the custom server name of tls certificate. By + // default, server name if same to ServerAddr. + TLSServerName string `ini:"tls_server_name" json:"tls_server_name"` // HeartBeatInterval specifies at what interval heartbeats are sent to the // server, in seconds. It is not recommended to change this value. By // default, this value is 30. - HeartbeatInterval int64 `json:"heartbeat_interval"` + HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"` // HeartBeatTimeout specifies the maximum allowed heartbeat response delay // before the connection is terminated, in seconds. It is not recommended // to change this value. By default, this value is 90. - HeartbeatTimeout int64 `json:"heartbeat_timeout"` + HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` // Client meta info - Metas map[string]string `json:"metas"` + Metas map[string]string `ini:"-" json:"metas"` // UDPPacketSize specifies the udp packet size // By default, this value is 1500 - UDPPacketSize int64 `json:"udp_packet_size"` + UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` } // GetDefaultClientConf returns a client configuration with default values. func GetDefaultClientConf() ClientCommonConf { return ClientCommonConf{ + ClientConfig: auth.GetDefaultClientConf(), ServerAddr: "0.0.0.0", ServerPort: 7000, HTTPProxy: os.Getenv("http_proxy"), @@ -154,7 +160,7 @@ func GetDefaultClientConf() ClientCommonConf { User: "", DNSServer: "", LoginFailExit: true, - Start: make(map[string]struct{}), + Start: make([]string, 0), Protocol: "tcp", TLSEnable: false, TLSCertFile: "", @@ -167,185 +173,13 @@ func GetDefaultClientConf() ClientCommonConf { } } -func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error) { - cfg = GetDefaultClientConf() - - conf, err := ini.Load(strings.NewReader(content)) - if err != nil { - return ClientCommonConf{}, fmt.Errorf("parse ini conf file error: %v", err) - } - - cfg.ClientConfig = auth.UnmarshalClientConfFromIni(conf) - - var ( - tmpStr string - ok bool - v int64 - ) - if tmpStr, ok = conf.Get("common", "server_addr"); ok { - cfg.ServerAddr = tmpStr - } - - if tmpStr, ok = conf.Get("common", "server_port"); ok { - v, err = strconv.ParseInt(tmpStr, 10, 64) - if err != nil { - err = fmt.Errorf("Parse conf error: invalid server_port") - return - } - cfg.ServerPort = int(v) - } - - if tmpStr, ok = conf.Get("common", "disable_log_color"); ok && tmpStr == "true" { - cfg.DisableLogColor = true - } - - if tmpStr, ok = conf.Get("common", "http_proxy"); ok { - cfg.HTTPProxy = tmpStr - } - - if tmpStr, ok = conf.Get("common", "log_file"); ok { - cfg.LogFile = tmpStr - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } - } - - if tmpStr, ok = conf.Get("common", "log_level"); ok { - cfg.LogLevel = tmpStr - } - - if tmpStr, ok = conf.Get("common", "log_max_days"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err == nil { - cfg.LogMaxDays = v - } - } - - if tmpStr, ok = conf.Get("common", "admin_addr"); ok { - cfg.AdminAddr = tmpStr - } - - if tmpStr, ok = conf.Get("common", "admin_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err == nil { - cfg.AdminPort = int(v) - } else { - err = fmt.Errorf("Parse conf error: invalid admin_port") - return - } - } - - if tmpStr, ok = conf.Get("common", "admin_user"); ok { - cfg.AdminUser = tmpStr - } - - if tmpStr, ok = conf.Get("common", "admin_pwd"); ok { - cfg.AdminPwd = tmpStr - } - - if tmpStr, ok = conf.Get("common", "assets_dir"); ok { - cfg.AssetsDir = tmpStr - } - - if tmpStr, ok = conf.Get("common", "pool_count"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err == nil { - cfg.PoolCount = int(v) - } - } - - if tmpStr, ok = conf.Get("common", "tcp_mux"); ok && tmpStr == "false" { - cfg.TCPMux = false - } else { - cfg.TCPMux = true - } - - if tmpStr, ok = conf.Get("common", "user"); ok { - cfg.User = tmpStr - } - - if tmpStr, ok = conf.Get("common", "dns_server"); ok { - cfg.DNSServer = tmpStr - } - - if tmpStr, ok = conf.Get("common", "start"); ok { - proxyNames := strings.Split(tmpStr, ",") - for _, name := range proxyNames { - cfg.Start[strings.TrimSpace(name)] = struct{}{} - } - } - - if tmpStr, ok = conf.Get("common", "login_fail_exit"); ok && tmpStr == "false" { - cfg.LoginFailExit = false - } else { - cfg.LoginFailExit = true - } - - if tmpStr, ok = conf.Get("common", "protocol"); ok { - // Now it only support tcp and kcp and websocket. - if tmpStr != "tcp" && tmpStr != "kcp" && tmpStr != "websocket" { - err = fmt.Errorf("Parse conf error: invalid protocol") - return - } - cfg.Protocol = tmpStr - } - - if tmpStr, ok = conf.Get("common", "tls_enable"); ok && tmpStr == "true" { - cfg.TLSEnable = true - } else { - cfg.TLSEnable = false - } - - if tmpStr, ok = conf.Get("common", "tls_cert_file"); ok { - cfg.TLSCertFile = tmpStr - } - - if tmpStr, ok := conf.Get("common", "tls_key_file"); ok { - cfg.TLSKeyFile = tmpStr - } - - if tmpStr, ok := conf.Get("common", "tls_trusted_ca_file"); ok { - cfg.TLSTrustedCaFile = tmpStr - } - - if tmpStr, ok = conf.Get("common", "heartbeat_timeout"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout") - return - } - cfg.HeartbeatTimeout = v - } - - if tmpStr, ok = conf.Get("common", "heartbeat_interval"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid heartbeat_interval") - return - } - cfg.HeartbeatInterval = v - } - for k, v := range conf.Section("common") { - if strings.HasPrefix(k, "meta_") { - cfg.Metas[strings.TrimPrefix(k, "meta_")] = v - } - } - if tmpStr, ok = conf.Get("common", "udp_packet_size"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid udp_packet_size") - return - } - cfg.UDPPacketSize = v - } - return -} - -func (cfg *ClientCommonConf) Check() (err error) { +func (cfg *ClientCommonConf) Check() error { if cfg.HeartbeatInterval <= 0 { - err = fmt.Errorf("Parse conf error: invalid heartbeat_interval") - return + return fmt.Errorf("Parse conf error: invalid heartbeat_interval") } if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { - err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") - return + return fmt.Errorf("Parse conf error: invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") } if cfg.TLSEnable == false { @@ -361,5 +195,178 @@ func (cfg *ClientCommonConf) Check() (err error) { fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false") } } - return + + return nil +} + +// Supported sources including: string(file path), []byte, Reader interface. +func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { + f, err := ini.LoadSources(ini.LoadOptions{ + Insensitive: false, + InsensitiveSections: false, + InsensitiveKeys: false, + IgnoreInlineComment: true, + AllowBooleanKeys: true, + }, source) + if err != nil { + return ClientCommonConf{}, err + } + + s, err := f.GetSection("common") + if err != nil { + return ClientCommonConf{}, fmt.Errorf("invalid configuration file, not found [common] section") + } + + common := GetDefaultClientConf() + err = s.MapTo(&common) + if err != nil { + return ClientCommonConf{}, err + } + + common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_") + + return common, nil +} + +// if len(startProxy) is 0, start all +// otherwise just start proxies in startProxy map +func LoadAllProxyConfsFromIni( + prefix string, + source interface{}, + start []string, +) (map[string]ProxyConf, map[string]VisitorConf, error) { + + f, err := ini.LoadSources(ini.LoadOptions{ + Insensitive: false, + InsensitiveSections: false, + InsensitiveKeys: false, + IgnoreInlineComment: true, + AllowBooleanKeys: true, + }, source) + if err != nil { + return nil, nil, err + } + + proxyConfs := make(map[string]ProxyConf) + visitorConfs := make(map[string]VisitorConf) + + if prefix != "" { + prefix += "." + } + + startProxy := make(map[string]struct{}) + for _, s := range start { + startProxy[s] = struct{}{} + } + + startAll := true + if len(startProxy) > 0 { + startAll = false + } + + // Build template sections from range section And append to ini.File. + rangeSections := make([]*ini.Section, 0) + for _, section := range f.Sections() { + + if !strings.HasPrefix(section.Name(), "range:") { + continue + } + + rangeSections = append(rangeSections, section) + } + + for _, section := range rangeSections { + err = renderRangeProxyTemplates(f, section) + if err != nil { + return nil, nil, fmt.Errorf("fail to render range-section[%s] with error: %v", section.Name(), err) + } + } + + for _, section := range f.Sections() { + name := section.Name() + + if name == ini.DefaultSection || name == "common" || strings.HasPrefix(name, "range:") { + continue + } + + _, shouldStart := startProxy[name] + if !startAll && !shouldStart { + continue + } + + roleType := section.Key("role").String() + if roleType == "" { + roleType = "server" + } + + switch roleType { + case "server": + newConf, newErr := NewProxyConfFromIni(prefix, name, section) + if newErr != nil { + return nil, nil, fmt.Errorf("fail to parse section[%s], err: %v", name, newErr) + } + proxyConfs[prefix+name] = newConf + case "visitor": + newConf, newErr := NewVisitorConfFromIni(prefix, name, section) + if newErr != nil { + return nil, nil, newErr + } + visitorConfs[prefix+name] = newConf + default: + return nil, nil, fmt.Errorf("section[%s] role should be 'server' or 'visitor'", name) + } + } + return proxyConfs, visitorConfs, nil +} + +func renderRangeProxyTemplates(f *ini.File, section *ini.Section) error { + + // Validation + localPortStr := section.Key("local_port").String() + remotePortStr := section.Key("remote_port").String() + if localPortStr == "" || remotePortStr == "" { + return fmt.Errorf("local_port or remote_port is empty") + } + + localPorts, err := util.ParseRangeNumbers(localPortStr) + if err != nil { + return err + } + + remotePorts, err := util.ParseRangeNumbers(remotePortStr) + if err != nil { + return err + } + + if len(localPorts) != len(remotePorts) { + return fmt.Errorf("local ports number should be same with remote ports number") + } + + if len(localPorts) == 0 { + return fmt.Errorf("local_port and remote_port is necessary") + } + + // Templates + prefix := strings.TrimSpace(strings.TrimPrefix(section.Name(), "range:")) + + for i := range localPorts { + tmpname := fmt.Sprintf("%s_%d", prefix, i) + + tmpsection, err := f.NewSection(tmpname) + if err != nil { + return err + } + + copySection(section, tmpsection) + tmpsection.NewKey("local_port", fmt.Sprintf("%d", localPorts[i])) + tmpsection.NewKey("remote_port", fmt.Sprintf("%d", remotePorts[i])) + } + + return nil +} + +func copySection(source, target *ini.Section) { + for key, value := range source.KeysHash() { + target.NewKey(key, value) + } } diff --git a/pkg/config/client_test.go b/pkg/config/client_test.go new file mode 100644 index 00000000..874421d1 --- /dev/null +++ b/pkg/config/client_test.go @@ -0,0 +1,645 @@ +// Copyright 2020 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 config + +import ( + "testing" + + "github.com/fatedier/frp/pkg/auth" + "github.com/fatedier/frp/pkg/consts" + + "github.com/stretchr/testify/assert" +) + +const ( + testUser = "test" +) + +var ( + testClientBytesWithFull = []byte(` + # [common] is integral section + [common] + server_addr = 0.0.0.9 + server_port = 7009 + http_proxy = http://user:passwd@192.168.1.128:8080 + log_file = ./frpc.log9 + log_way = file + log_level = info9 + log_max_days = 39 + disable_log_color = false + authenticate_heartbeats = false + authenticate_new_work_conns = false + token = 12345678 + oidc_client_id = client-id + oidc_client_secret = client-secret + oidc_audience = audience + oidc_token_endpoint_url = endpoint_url + admin_addr = 127.0.0.9 + admin_port = 7409 + admin_user = admin9 + admin_pwd = admin9 + assets_dir = ./static9 + pool_count = 59 + tcp_mux + user = your_name + login_fail_exit + protocol = tcp + tls_enable = true + tls_cert_file = client.crt + tls_key_file = client.key + tls_trusted_ca_file = ca.crt + tls_server_name = example.com + dns_server = 8.8.8.9 + start = ssh,dns + heartbeat_interval = 39 + heartbeat_timeout = 99 + meta_var1 = 123 + meta_var2 = 234 + udp_packet_size = 1509 + + # all proxy + [ssh] + type = tcp + local_ip = 127.0.0.9 + local_port = 29 + bandwidth_limit = 19MB + use_encryption + use_compression + remote_port = 6009 + group = test_group + group_key = 123456 + health_check_type = tcp + health_check_timeout_s = 3 + health_check_max_failed = 3 + health_check_interval_s = 19 + meta_var1 = 123 + meta_var2 = 234 + + [ssh_random] + type = tcp + local_ip = 127.0.0.9 + local_port = 29 + remote_port = 9 + + [range:tcp_port] + type = tcp + local_ip = 127.0.0.9 + local_port = 6010-6011,6019 + remote_port = 6010-6011,6019 + use_encryption = false + use_compression = false + + [dns] + type = udp + local_ip = 114.114.114.114 + local_port = 59 + remote_port = 6009 + use_encryption + use_compression + + [range:udp_port] + type = udp + local_ip = 114.114.114.114 + local_port = 6000,6010-6011 + remote_port = 6000,6010-6011 + use_encryption + use_compression + + [web01] + type = http + local_ip = 127.0.0.9 + local_port = 89 + use_encryption + use_compression + http_user = admin + http_pwd = admin + subdomain = web01 + custom_domains = web02.yourdomain.com + locations = /,/pic + host_header_rewrite = example.com + header_X-From-Where = frp + health_check_type = http + health_check_url = /status + health_check_interval_s = 19 + health_check_max_failed = 3 + health_check_timeout_s = 3 + + [web02] + type = https + local_ip = 127.0.0.9 + local_port = 8009 + use_encryption + use_compression + subdomain = web01 + custom_domains = web02.yourdomain.com + proxy_protocol_version = v2 + + [secret_tcp] + type = stcp + sk = abcdefg + local_ip = 127.0.0.1 + local_port = 22 + use_encryption = false + use_compression = false + + [p2p_tcp] + type = xtcp + sk = abcdefg + local_ip = 127.0.0.1 + local_port = 22 + use_encryption = false + use_compression = false + + [tcpmuxhttpconnect] + type = tcpmux + multiplexer = httpconnect + local_ip = 127.0.0.1 + local_port = 10701 + custom_domains = tunnel1 + + [plugin_unix_domain_socket] + type = tcp + remote_port = 6003 + plugin = unix_domain_socket + plugin_unix_path = /var/run/docker.sock + + [plugin_http_proxy] + type = tcp + remote_port = 6004 + plugin = http_proxy + plugin_http_user = abc + plugin_http_passwd = abc + + [plugin_socks5] + type = tcp + remote_port = 6005 + plugin = socks5 + plugin_user = abc + plugin_passwd = abc + + [plugin_static_file] + type = tcp + remote_port = 6006 + plugin = static_file + plugin_local_path = /var/www/blog + plugin_strip_prefix = static + plugin_http_user = abc + plugin_http_passwd = abc + + [plugin_https2http] + type = https + custom_domains = test.yourdomain.com + plugin = https2http + plugin_local_addr = 127.0.0.1:80 + plugin_crt_path = ./server.crt + plugin_key_path = ./server.key + plugin_host_header_rewrite = 127.0.0.1 + plugin_header_X-From-Where = frp + + [plugin_http2https] + type = http + custom_domains = test.yourdomain.com + plugin = http2https + plugin_local_addr = 127.0.0.1:443 + plugin_host_header_rewrite = 127.0.0.1 + plugin_header_X-From-Where = frp + + # visitor + [secret_tcp_visitor] + role = visitor + type = stcp + server_name = secret_tcp + sk = abcdefg + bind_addr = 127.0.0.1 + bind_port = 9000 + use_encryption = false + use_compression = false + + [p2p_tcp_visitor] + role = visitor + type = xtcp + server_name = p2p_tcp + sk = abcdefg + bind_addr = 127.0.0.1 + bind_port = 9001 + use_encryption = false + use_compression = false + `) +) + +func Test_LoadClientCommonConf(t *testing.T) { + assert := assert.New(t) + + expected := ClientCommonConf{ + ClientConfig: auth.ClientConfig{ + BaseConfig: auth.BaseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + }, + TokenConfig: auth.TokenConfig{ + Token: "12345678", + }, + OidcClientConfig: auth.OidcClientConfig{ + OidcClientID: "client-id", + OidcClientSecret: "client-secret", + OidcAudience: "audience", + OidcTokenEndpointURL: "endpoint_url", + }, + }, + ServerAddr: "0.0.0.9", + ServerPort: 7009, + HTTPProxy: "http://user:passwd@192.168.1.128:8080", + LogFile: "./frpc.log9", + LogWay: "file", + LogLevel: "info9", + LogMaxDays: 39, + DisableLogColor: false, + AdminAddr: "127.0.0.9", + AdminPort: 7409, + AdminUser: "admin9", + AdminPwd: "admin9", + AssetsDir: "./static9", + PoolCount: 59, + TCPMux: true, + User: "your_name", + LoginFailExit: true, + Protocol: "tcp", + TLSEnable: true, + TLSCertFile: "client.crt", + TLSKeyFile: "client.key", + TLSTrustedCaFile: "ca.crt", + TLSServerName: "example.com", + DNSServer: "8.8.8.9", + Start: []string{"ssh", "dns"}, + HeartbeatInterval: 39, + HeartbeatTimeout: 99, + Metas: map[string]string{ + "var1": "123", + "var2": "234", + }, + UDPPacketSize: 1509, + } + + common, err := UnmarshalClientConfFromIni(testClientBytesWithFull) + assert.NoError(err) + assert.Equal(expected, common) +} + +func Test_LoadClientBasicConf(t *testing.T) { + assert := assert.New(t) + + proxyExpected := map[string]ProxyConf{ + testUser + ".ssh": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".ssh", + ProxyType: consts.TCPProxy, + UseCompression: true, + UseEncryption: true, + Group: "test_group", + GroupKey: "123456", + BandwidthLimit: MustBandwidthQuantity("19MB"), + Metas: map[string]string{ + "var1": "123", + "var2": "234", + }, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 29, + }, + HealthCheckConf: HealthCheckConf{ + HealthCheckType: consts.TCPProxy, + HealthCheckTimeoutS: 3, + HealthCheckMaxFailed: 3, + HealthCheckIntervalS: 19, + HealthCheckAddr: "127.0.0.9:29", + }, + }, + RemotePort: 6009, + }, + testUser + ".ssh_random": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".ssh_random", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 29, + }, + }, + RemotePort: 9, + }, + testUser + ".tcp_port_0": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".tcp_port_0", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6010, + }, + }, + RemotePort: 6010, + }, + testUser + ".tcp_port_1": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".tcp_port_1", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6011, + }, + }, + RemotePort: 6011, + }, + testUser + ".tcp_port_2": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".tcp_port_2", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6019, + }, + }, + RemotePort: 6019, + }, + testUser + ".dns": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".dns", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 59, + }, + }, + RemotePort: 6009, + }, + testUser + ".udp_port_0": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".udp_port_0", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6000, + }, + }, + RemotePort: 6000, + }, + testUser + ".udp_port_1": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".udp_port_1", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6010, + }, + }, + RemotePort: 6010, + }, + testUser + ".udp_port_2": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".udp_port_2", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6011, + }, + }, + RemotePort: 6011, + }, + testUser + ".web01": &HTTPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".web01", + ProxyType: consts.HTTPProxy, + UseCompression: true, + UseEncryption: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 89, + }, + HealthCheckConf: HealthCheckConf{ + HealthCheckType: consts.HTTPProxy, + HealthCheckTimeoutS: 3, + HealthCheckMaxFailed: 3, + HealthCheckIntervalS: 19, + HealthCheckURL: "http://127.0.0.9:89/status", + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"web02.yourdomain.com"}, + SubDomain: "web01", + }, + Locations: []string{"/", "/pic"}, + HTTPUser: "admin", + HTTPPwd: "admin", + HostHeaderRewrite: "example.com", + Headers: map[string]string{ + "X-From-Where": "frp", + }, + }, + testUser + ".web02": &HTTPSProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".web02", + ProxyType: consts.HTTPSProxy, + UseCompression: true, + UseEncryption: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 8009, + }, + ProxyProtocolVersion: "v2", + }, + DomainConf: DomainConf{ + CustomDomains: []string{"web02.yourdomain.com"}, + SubDomain: "web01", + }, + }, + testUser + ".secret_tcp": &STCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".secret_tcp", + ProxyType: consts.STCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 22, + }, + }, + Role: "server", + Sk: "abcdefg", + }, + testUser + ".p2p_tcp": &XTCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".p2p_tcp", + ProxyType: consts.XTCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 22, + }, + }, + Role: "server", + Sk: "abcdefg", + }, + testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".tcpmuxhttpconnect", + ProxyType: consts.TCPMuxProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 10701, + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"tunnel1"}, + SubDomain: "", + }, + Multiplexer: "httpconnect", + }, + testUser + ".plugin_unix_domain_socket": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_unix_domain_socket", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "unix_domain_socket", + PluginParams: map[string]string{ + "plugin_unix_path": "/var/run/docker.sock", + }, + }, + }, + RemotePort: 6003, + }, + testUser + ".plugin_http_proxy": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_http_proxy", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "http_proxy", + PluginParams: map[string]string{ + "plugin_http_user": "abc", + "plugin_http_passwd": "abc", + }, + }, + }, + RemotePort: 6004, + }, + testUser + ".plugin_socks5": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_socks5", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "socks5", + PluginParams: map[string]string{ + "plugin_user": "abc", + "plugin_passwd": "abc", + }, + }, + }, + RemotePort: 6005, + }, + testUser + ".plugin_static_file": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_static_file", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "static_file", + PluginParams: map[string]string{ + "plugin_local_path": "/var/www/blog", + "plugin_strip_prefix": "static", + "plugin_http_user": "abc", + "plugin_http_passwd": "abc", + }, + }, + }, + RemotePort: 6006, + }, + testUser + ".plugin_https2http": &HTTPSProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_https2http", + ProxyType: consts.HTTPSProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "https2http", + PluginParams: map[string]string{ + "plugin_local_addr": "127.0.0.1:80", + "plugin_crt_path": "./server.crt", + "plugin_key_path": "./server.key", + "plugin_host_header_rewrite": "127.0.0.1", + "plugin_header_X-From-Where": "frp", + }, + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"test.yourdomain.com"}, + }, + }, + testUser + ".plugin_http2https": &HTTPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testUser + ".plugin_http2https", + ProxyType: consts.HTTPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + Plugin: "http2https", + PluginParams: map[string]string{ + "plugin_local_addr": "127.0.0.1:443", + "plugin_host_header_rewrite": "127.0.0.1", + "plugin_header_X-From-Where": "frp", + }, + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"test.yourdomain.com"}, + }, + }, + } + + visitorExpected := map[string]VisitorConf{ + testUser + ".secret_tcp_visitor": &STCPVisitorConf{ + BaseVisitorConf: BaseVisitorConf{ + ProxyName: testUser + ".secret_tcp_visitor", + ProxyType: consts.STCPProxy, + Role: "visitor", + Sk: "abcdefg", + ServerName: testVisitorPrefix + "secret_tcp", + BindAddr: "127.0.0.1", + BindPort: 9000, + }, + }, + testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{ + BaseVisitorConf: BaseVisitorConf{ + ProxyName: testUser + ".p2p_tcp_visitor", + ProxyType: consts.XTCPProxy, + Role: "visitor", + Sk: "abcdefg", + ServerName: testProxyPrefix + "p2p_tcp", + BindAddr: "127.0.0.1", + BindPort: 9001, + }, + }, + } + + proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil) + assert.NoError(err) + assert.Equal(proxyExpected, proxyActual) + assert.Equal(visitorExpected, visitorActual) + +} diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go index ad5b64b7..96a4face 100644 --- a/pkg/config/proxy.go +++ b/pkg/config/proxy.go @@ -17,34 +17,28 @@ package config import ( "fmt" "reflect" - "strconv" "strings" "github.com/fatedier/frp/pkg/consts" "github.com/fatedier/frp/pkg/msg" - "github.com/fatedier/frp/pkg/util/util" - ini "github.com/vaughan0/go-ini" + "gopkg.in/ini.v1" ) +// Proxy var ( - proxyConfTypeMap map[string]reflect.Type + proxyConfTypeMap = map[string]reflect.Type{ + consts.TCPProxy: reflect.TypeOf(TCPProxyConf{}), + consts.TCPMuxProxy: reflect.TypeOf(TCPMuxProxyConf{}), + consts.UDPProxy: reflect.TypeOf(UDPProxyConf{}), + consts.HTTPProxy: reflect.TypeOf(HTTPProxyConf{}), + consts.HTTPSProxy: reflect.TypeOf(HTTPSProxyConf{}), + consts.STCPProxy: reflect.TypeOf(STCPProxyConf{}), + consts.XTCPProxy: reflect.TypeOf(XTCPProxyConf{}), + consts.SUDPProxy: reflect.TypeOf(SUDPProxyConf{}), + } ) -func init() { - proxyConfTypeMap = make(map[string]reflect.Type) - proxyConfTypeMap[consts.TCPProxy] = reflect.TypeOf(TCPProxyConf{}) - proxyConfTypeMap[consts.TCPMuxProxy] = reflect.TypeOf(TCPMuxProxyConf{}) - proxyConfTypeMap[consts.UDPProxy] = reflect.TypeOf(UDPProxyConf{}) - proxyConfTypeMap[consts.HTTPProxy] = reflect.TypeOf(HTTPProxyConf{}) - proxyConfTypeMap[consts.HTTPSProxy] = reflect.TypeOf(HTTPSProxyConf{}) - proxyConfTypeMap[consts.STCPProxy] = reflect.TypeOf(STCPProxyConf{}) - proxyConfTypeMap[consts.XTCPProxy] = reflect.TypeOf(XTCPProxyConf{}) - proxyConfTypeMap[consts.SUDPProxy] = reflect.TypeOf(SUDPProxyConf{}) -} - -// NewConfByType creates a empty ProxyConf object by proxyType. -// If proxyType isn't exist, return nil. func NewConfByType(proxyType string) ProxyConf { v, ok := proxyConfTypeMap[proxyType] if !ok { @@ -56,87 +50,273 @@ func NewConfByType(proxyType string) ProxyConf { type ProxyConf interface { GetBaseInfo() *BaseProxyConf - UnmarshalFromMsg(pMsg *msg.NewProxy) - UnmarshalFromIni(prefix string, name string, conf ini.Section) error - MarshalToMsg(pMsg *msg.NewProxy) + UnmarshalFromMsg(*msg.NewProxy) + UnmarshalFromIni(string, string, *ini.Section) error + MarshalToMsg(*msg.NewProxy) CheckForCli() error - CheckForSvr(serverCfg ServerCommonConf) error - Compare(conf ProxyConf) bool + CheckForSvr(ServerCommonConf) error + Compare(ProxyConf) bool } -func NewProxyConfFromMsg(pMsg *msg.NewProxy, serverCfg ServerCommonConf) (cfg ProxyConf, err error) { - if pMsg.ProxyType == "" { - pMsg.ProxyType = consts.TCPProxy - } +// LocalSvrConf configures what location the client will to, or what +// plugin will be used. +type LocalSvrConf struct { + // LocalIP specifies the IP address or host name to to. + LocalIP string `ini:"local_ip" json:"local_ip"` + // LocalPort specifies the port to to. + LocalPort int `ini:"local_port" json:"local_port"` - cfg = NewConfByType(pMsg.ProxyType) - if cfg == nil { - err = fmt.Errorf("proxy [%s] type [%s] error", pMsg.ProxyName, pMsg.ProxyType) - return - } - cfg.UnmarshalFromMsg(pMsg) - err = cfg.CheckForSvr(serverCfg) - return + // Plugin specifies what plugin should be used for ng. If this value + // is set, the LocalIp and LocalPort values will be ignored. By default, + // this value is "". + Plugin string `ini:"plugin" json:"plugin"` + // PluginParams specify parameters to be passed to the plugin, if one is + // being used. By default, this value is an empty map. + PluginParams map[string]string `ini:"-"` } -func NewProxyConfFromIni(prefix string, name string, section ini.Section) (cfg ProxyConf, err error) { - proxyType := section["type"] - if proxyType == "" { - proxyType = consts.TCPProxy - section["type"] = consts.TCPProxy - } - cfg = NewConfByType(proxyType) - if cfg == nil { - err = fmt.Errorf("proxy [%s] type [%s] error", name, proxyType) - return - } - if err = cfg.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - if err = cfg.CheckForCli(); err != nil { - return - } - return +// HealthCheckConf configures health checking. This can be useful for load +// balancing purposes to detect and remove proxies to failing services. +type HealthCheckConf struct { + // HealthCheckType specifies what protocol to use for health checking. + // Valid values include "tcp", "http", and "". If this value is "", health + // checking will not be performed. By default, this value is "". + // + // If the type is "tcp", a connection will be attempted to the target + // server. If a connection cannot be established, the health check fails. + // + // If the type is "http", a GET request will be made to the endpoint + // specified by HealthCheckURL. If the response is not a 200, the health + // check fails. + HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http + // HealthCheckTimeoutS specifies the number of seconds to wait for a health + // check attempt to connect. If the timeout is reached, this counts as a + // health check failure. By default, this value is 3. + HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"` + // HealthCheckMaxFailed specifies the number of allowed failures before the + // is stopped. By default, this value is 1. + HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"` + // HealthCheckIntervalS specifies the time in seconds between health + // checks. By default, this value is 10. + HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"` + // HealthCheckURL specifies the address to send health checks to if the + // health check type is "http". + HealthCheckURL string `ini:"health_check_url" json:"health_check_url"` + // HealthCheckAddr specifies the address to connect to if the health check + // type is "tcp". + HealthCheckAddr string `ini:"-"` } -// BaseProxyConf provides configuration info that is common to all proxy types. +// BaseProxyConf provides configuration info that is common to all types. type BaseProxyConf struct { - // ProxyName is the name of this proxy. - ProxyName string `json:"proxy_name"` - // ProxyType specifies the type of this proxy. Valid values include "tcp", + // ProxyName is the name of this + ProxyName string `ini:"name" json:"name"` + // ProxyType specifies the type of this Valid values include "tcp", // "udp", "http", "https", "stcp", and "xtcp". By default, this value is // "tcp". - ProxyType string `json:"proxy_type"` + ProxyType string `ini:"type" json:"type"` // UseEncryption controls whether or not communication with the server will // be encrypted. Encryption is done using the tokens supplied in the server // and client configuration. By default, this value is false. - UseEncryption bool `json:"use_encryption"` + UseEncryption bool `ini:"use_encryption" json:"use_encryption"` // UseCompression controls whether or not communication with the server // will be compressed. By default, this value is false. - UseCompression bool `json:"use_compression"` - // Group specifies which group the proxy is a part of. The server will use + UseCompression bool `ini:"use_compression" json:"use_compression"` + // Group specifies which group the is a part of. The server will use // this information to load balance proxies in the same group. If the value - // is "", this proxy will not be in a group. By default, this value is "". - Group string `json:"group"` + // is "", this will not be in a group. By default, this value is "". + Group string `ini:"group" json:"group"` // GroupKey specifies a group key, which should be the same among proxies // of the same group. By default, this value is "". - GroupKey string `json:"group_key"` + GroupKey string `ini:"group_key" json:"group_key"` // ProxyProtocolVersion specifies which protocol version to use. Valid // values include "v1", "v2", and "". If the value is "", a protocol // version will be automatically selected. By default, this value is "". - ProxyProtocolVersion string `json:"proxy_protocol_version"` + ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"` - // BandwidthLimit limit the proxy bandwidth + // BandwidthLimit limit the bandwidth // 0 means no limit - BandwidthLimit BandwidthQuantity `json:"bandwidth_limit"` + BandwidthLimit BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"` // meta info for each proxy - Metas map[string]string `json:"metas"` + Metas map[string]string `ini:"-" json:"metas"` - LocalSvrConf - HealthCheckConf + // TODO: LocalSvrConf => LocalAppConf + LocalSvrConf `ini:",extends" json:"inline"` + HealthCheckConf `ini:",extends" json:"inline"` +} + +type DomainConf struct { + CustomDomains []string `ini:"custom_domains" json:"custom_domains"` + SubDomain string `ini:"subdomain" json:"subdomain"` +} + +// HTTP +type HTTPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + DomainConf `ini:",extends" json:"inline"` + + Locations []string `ini:"locations" json:"locations"` + HTTPUser string `ini:"http_user" json:"http_user"` + HTTPPwd string `ini:"http_pwd" json:"http_pwd"` + HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"` + Headers map[string]string `ini:"-" json:"headers"` +} + +// HTTPS +type HTTPSProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + DomainConf `ini:",extends" json:"inline"` +} + +// TCP +type TCPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +// TCPMux +type TCPMuxProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + DomainConf `ini:",extends" json:"inline"` + + Multiplexer string `ini:"multiplexer"` +} + +// STCP +type STCPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` +} + +// XTCP +type XTCPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` +} + +// UDP +type UDPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +// SUDP +type SUDPProxyConf struct { + BaseProxyConf `ini:",extends" json:"inline"` + + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` +} + +// Proxy Conf Loader +// DefaultProxyConf creates a empty ProxyConf object by proxyType. +// If proxyType doesn't exist, return nil. +func DefaultProxyConf(proxyType string) ProxyConf { + var conf ProxyConf + switch proxyType { + case consts.TCPProxy: + conf = &TCPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + } + case consts.TCPMuxProxy: + conf = &TCPMuxProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + } + case consts.UDPProxy: + conf = &UDPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + } + case consts.HTTPProxy: + conf = &HTTPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + } + case consts.HTTPSProxy: + conf = &HTTPSProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + } + case consts.STCPProxy: + conf = &STCPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + Role: "server", + } + case consts.XTCPProxy: + conf = &XTCPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + Role: "server", + } + case consts.SUDPProxy: + conf = &SUDPProxyConf{ + BaseProxyConf: defaultBaseProxyConf(proxyType), + Role: "server", + } + default: + return nil + } + + return conf +} + +// Proxy loaded from ini +func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) { + // section.Key: if key not exists, section will set it with default value. + proxyType := section.Key("type").String() + if proxyType == "" { + proxyType = consts.TCPProxy + } + + conf := DefaultProxyConf(proxyType) + if conf == nil { + return nil, fmt.Errorf("proxy [%s] type [%s] error", name, proxyType) + } + + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, err + } + + if err := conf.CheckForCli(); err != nil { + return nil, err + } + + return conf, nil +} + +// Proxy loaded from msg +func NewProxyConfFromMsg(pMsg *msg.NewProxy, serverCfg ServerCommonConf) (ProxyConf, error) { + if pMsg.ProxyType == "" { + pMsg.ProxyType = consts.TCPProxy + } + + conf := DefaultProxyConf(pMsg.ProxyType) + if conf == nil { + return nil, fmt.Errorf("proxy [%s] type [%s] error", pMsg.ProxyName, pMsg.ProxyType) + } + + conf.UnmarshalFromMsg(pMsg) + + err := conf.CheckForSvr(serverCfg) + if err != nil { + return nil, err + } + + return conf, nil +} + +// Base +func defaultBaseProxyConf(proxyType string) BaseProxyConf { + return BaseProxyConf{ + ProxyType: proxyType, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + }, + } } func (cfg *BaseProxyConf) GetBaseInfo() *BaseProxyConf { @@ -155,63 +335,41 @@ func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool { !reflect.DeepEqual(cfg.Metas, cmp.Metas) { return false } - if !cfg.LocalSvrConf.compare(&cmp.LocalSvrConf) { + + if !reflect.DeepEqual(cfg.LocalSvrConf, cmp.LocalSvrConf) { return false } - if !cfg.HealthCheckConf.compare(&cmp.HealthCheckConf) { + if !reflect.DeepEqual(cfg.HealthCheckConf, cmp.HealthCheckConf) { return false } + return true } -func (cfg *BaseProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.ProxyName = pMsg.ProxyName - cfg.ProxyType = pMsg.ProxyType - cfg.UseEncryption = pMsg.UseEncryption - cfg.UseCompression = pMsg.UseCompression - cfg.Group = pMsg.Group - cfg.GroupKey = pMsg.GroupKey - cfg.Metas = pMsg.Metas -} - -func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) error { - var ( - tmpStr string - ok bool - err error - ) +// BaseProxyConf apply custom logic changes. +func (cfg *BaseProxyConf) decorate(prefix string, name string, section *ini.Section) error { + // proxy_name cfg.ProxyName = prefix + name - cfg.ProxyType = section["type"] - tmpStr, ok = section["use_encryption"] - if ok && tmpStr == "true" { - cfg.UseEncryption = true + // metas_xxx + cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_") + + // bandwidth_limit + if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil { + cfg.BandwidthLimit, err = NewBandwidthQuantity(bandwidth.String()) + if err != nil { + return err + } } - tmpStr, ok = section["use_compression"] - if ok && tmpStr == "true" { - cfg.UseCompression = true - } - - cfg.Group = section["group"] - cfg.GroupKey = section["group_key"] - cfg.ProxyProtocolVersion = section["proxy_protocol_version"] - - if cfg.BandwidthLimit, err = NewBandwidthQuantity(section["bandwidth_limit"]); err != nil { - return err - } - - if err = cfg.LocalSvrConf.UnmarshalFromIni(prefix, name, section); err != nil { - return err - } - - if err = cfg.HealthCheckConf.UnmarshalFromIni(prefix, name, section); err != nil { - return err - } + // plugin_xxx + cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") + // custom logic code if cfg.HealthCheckType == "tcp" && cfg.Plugin == "" { cfg.HealthCheckAddr = cfg.LocalIP + fmt.Sprintf(":%d", cfg.LocalPort) } + if cfg.HealthCheckType == "http" && cfg.Plugin == "" && cfg.HealthCheckURL != "" { s := fmt.Sprintf("http://%s:%d", cfg.LocalIP, cfg.LocalPort) if !strings.HasPrefix(cfg.HealthCheckURL, "/") { @@ -220,16 +378,10 @@ func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section i cfg.HealthCheckURL = s + cfg.HealthCheckURL } - cfg.Metas = make(map[string]string) - for k, v := range section { - if strings.HasPrefix(k, "meta_") { - cfg.Metas[strings.TrimPrefix(k, "meta_")] = v - } - } return nil } -func (cfg *BaseProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { +func (cfg *BaseProxyConf) marshalToMsg(pMsg *msg.NewProxy) { pMsg.ProxyName = cfg.ProxyName pMsg.ProxyType = cfg.ProxyType pMsg.UseEncryption = cfg.UseEncryption @@ -239,6 +391,16 @@ func (cfg *BaseProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { pMsg.Metas = cfg.Metas } +func (cfg *BaseProxyConf) unmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.ProxyName = pMsg.ProxyName + cfg.ProxyType = pMsg.ProxyType + cfg.UseEncryption = pMsg.UseEncryption + cfg.UseCompression = pMsg.UseCompression + cfg.Group = pMsg.Group + cfg.GroupKey = pMsg.GroupKey + cfg.Metas = pMsg.Metas +} + func (cfg *BaseProxyConf) checkForCli() (err error) { if cfg.ProxyProtocolVersion != "" { if cfg.ProxyProtocolVersion != "v1" && cfg.ProxyProtocolVersion != "v2" { @@ -255,85 +417,11 @@ func (cfg *BaseProxyConf) checkForCli() (err error) { return nil } -// Bind info -type BindInfoConf struct { - RemotePort int `json:"remote_port"` -} - -func (cfg *BindInfoConf) compare(cmp *BindInfoConf) bool { - if cfg.RemotePort != cmp.RemotePort { - return false - } - return true -} - -func (cfg *BindInfoConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.RemotePort = pMsg.RemotePort -} - -func (cfg *BindInfoConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - var ( - tmpStr string - ok bool - v int64 - ) - if tmpStr, ok = section["remote_port"]; ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] remote_port error", name) - } - cfg.RemotePort = int(v) - } else { - return fmt.Errorf("Parse conf error: proxy [%s] remote_port not found", name) - } +func (cfg *BaseProxyConf) checkForSvr(conf ServerCommonConf) error { return nil } -func (cfg *BindInfoConf) MarshalToMsg(pMsg *msg.NewProxy) { - pMsg.RemotePort = cfg.RemotePort -} - -// Domain info -type DomainConf struct { - CustomDomains []string `json:"custom_domains"` - SubDomain string `json:"sub_domain"` -} - -func (cfg *DomainConf) compare(cmp *DomainConf) bool { - if strings.Join(cfg.CustomDomains, " ") != strings.Join(cmp.CustomDomains, " ") || - cfg.SubDomain != cmp.SubDomain { - return false - } - return true -} - -func (cfg *DomainConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.CustomDomains = pMsg.CustomDomains - cfg.SubDomain = pMsg.SubDomain -} - -func (cfg *DomainConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - var ( - tmpStr string - ok bool - ) - if tmpStr, ok = section["custom_domains"]; ok { - cfg.CustomDomains = strings.Split(tmpStr, ",") - for i, domain := range cfg.CustomDomains { - cfg.CustomDomains[i] = strings.ToLower(strings.TrimSpace(domain)) - } - } - - if tmpStr, ok = section["subdomain"]; ok { - cfg.SubDomain = tmpStr - } - return -} - -func (cfg *DomainConf) MarshalToMsg(pMsg *msg.NewProxy) { - pMsg.CustomDomains = cfg.CustomDomains - pMsg.SubDomain = cfg.SubDomain -} - +// DomainConf func (cfg *DomainConf) check() (err error) { if len(cfg.CustomDomains) == 0 && cfg.SubDomain == "" { err = fmt.Errorf("custom_domains and subdomain should set at least one of them") @@ -370,70 +458,10 @@ func (cfg *DomainConf) checkForSvr(serverCfg ServerCommonConf) (err error) { return fmt.Errorf("'.' and '*' is not supported in subdomain") } } - return -} - -// LocalSvrConf configures what location the client will proxy to, or what -// plugin will be used. -type LocalSvrConf struct { - // LocalIP specifies the IP address or host name to proxy to. - LocalIP string `json:"local_ip"` - // LocalPort specifies the port to proxy to. - LocalPort int `json:"local_port"` - - // Plugin specifies what plugin should be used for proxying. If this value - // is set, the LocalIp and LocalPort values will be ignored. By default, - // this value is "". - Plugin string `json:"plugin"` - // PluginParams specify parameters to be passed to the plugin, if one is - // being used. By default, this value is an empty map. - PluginParams map[string]string `json:"plugin_params"` -} - -func (cfg *LocalSvrConf) compare(cmp *LocalSvrConf) bool { - if cfg.LocalIP != cmp.LocalIP || - cfg.LocalPort != cmp.LocalPort { - return false - } - if cfg.Plugin != cmp.Plugin || - len(cfg.PluginParams) != len(cmp.PluginParams) { - return false - } - for k, v := range cfg.PluginParams { - value, ok := cmp.PluginParams[k] - if !ok || v != value { - return false - } - } - return true -} - -func (cfg *LocalSvrConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - cfg.Plugin = section["plugin"] - cfg.PluginParams = make(map[string]string) - if cfg.Plugin != "" { - // get params begin with "plugin_" - for k, v := range section { - if strings.HasPrefix(k, "plugin_") { - cfg.PluginParams[k] = v - } - } - } else { - if cfg.LocalIP = section["local_ip"]; cfg.LocalIP == "" { - cfg.LocalIP = "127.0.0.1" - } - - if tmpStr, ok := section["local_port"]; ok { - if cfg.LocalPort, err = strconv.Atoi(tmpStr); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] local_port error", name) - } - } else { - return fmt.Errorf("Parse conf error: proxy [%s] local_port not found", name) - } - } - return + return nil } +// LocalSvrConf func (cfg *LocalSvrConf) checkForCli() (err error) { if cfg.Plugin == "" { if cfg.LocalIP == "" { @@ -448,73 +476,7 @@ func (cfg *LocalSvrConf) checkForCli() (err error) { return } -// HealthCheckConf configures health checking. This can be useful for load -// balancing purposes to detect and remove proxies to failing services. -type HealthCheckConf struct { - // HealthCheckType specifies what protocol to use for health checking. - // Valid values include "tcp", "http", and "". If this value is "", health - // checking will not be performed. By default, this value is "". - // - // If the type is "tcp", a connection will be attempted to the target - // server. If a connection cannot be established, the health check fails. - // - // If the type is "http", a GET request will be made to the endpoint - // specified by HealthCheckURL. If the response is not a 200, the health - // check fails. - HealthCheckType string `json:"health_check_type"` // tcp | http - // HealthCheckTimeoutS specifies the number of seconds to wait for a health - // check attempt to connect. If the timeout is reached, this counts as a - // health check failure. By default, this value is 3. - HealthCheckTimeoutS int `json:"health_check_timeout_s"` - // HealthCheckMaxFailed specifies the number of allowed failures before the - // proxy is stopped. By default, this value is 1. - HealthCheckMaxFailed int `json:"health_check_max_failed"` - // HealthCheckIntervalS specifies the time in seconds between health - // checks. By default, this value is 10. - HealthCheckIntervalS int `json:"health_check_interval_s"` - // HealthCheckURL specifies the address to send health checks to if the - // health check type is "http". - HealthCheckURL string `json:"health_check_url"` - // HealthCheckAddr specifies the address to connect to if the health check - // type is "tcp". - HealthCheckAddr string `json:"-"` -} - -func (cfg *HealthCheckConf) compare(cmp *HealthCheckConf) bool { - if cfg.HealthCheckType != cmp.HealthCheckType || - cfg.HealthCheckTimeoutS != cmp.HealthCheckTimeoutS || - cfg.HealthCheckMaxFailed != cmp.HealthCheckMaxFailed || - cfg.HealthCheckIntervalS != cmp.HealthCheckIntervalS || - cfg.HealthCheckURL != cmp.HealthCheckURL { - return false - } - return true -} - -func (cfg *HealthCheckConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - cfg.HealthCheckType = section["health_check_type"] - cfg.HealthCheckURL = section["health_check_url"] - - if tmpStr, ok := section["health_check_timeout_s"]; ok { - if cfg.HealthCheckTimeoutS, err = strconv.Atoi(tmpStr); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] health_check_timeout_s error", name) - } - } - - if tmpStr, ok := section["health_check_max_failed"]; ok { - if cfg.HealthCheckMaxFailed, err = strconv.Atoi(tmpStr); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] health_check_max_failed error", name) - } - } - - if tmpStr, ok := section["health_check_interval_s"]; ok { - if cfg.HealthCheckIntervalS, err = strconv.Atoi(tmpStr); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] health_check_interval_s error", name) - } - } - return -} - +// HealthCheckConf func (cfg *HealthCheckConf) checkForCli() error { if cfg.HealthCheckType != "" && cfg.HealthCheckType != "tcp" && cfg.HealthCheckType != "http" { return fmt.Errorf("unsupport health check type") @@ -527,113 +489,144 @@ func (cfg *HealthCheckConf) checkForCli() error { return nil } -// TCP -type TCPProxyConf struct { - BaseProxyConf - BindInfoConf +func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error { + err := section.MapTo(cfg) + if err != nil { + return err + } + + err = cfg.GetBaseInfo().decorate(prefix, name, section) + if err != nil { + return err + } + + return nil } +// TCP func (cfg *TCPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*TCPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) { + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { return false } + + // Add custom logic equal if exists. + if cfg.RemotePort != cmpConf.RemotePort { + return false + } + return true } func (cfg *TCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) - cfg.BindInfoConf.UnmarshalFromMsg(pMsg) + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists + cfg.RemotePort = pMsg.RemotePort } -func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return +func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err } - if err = cfg.BindInfoConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return + + // Add custom logic unmarshal if exists + + return nil } func (cfg *TCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - cfg.BindInfoConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists + pMsg.RemotePort = cfg.RemotePort } func (cfg *TCPProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return err + return } + + // Add custom logic check if exists + return } -func (cfg *TCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { return nil } - -// TCP Multiplexer -type TCPMuxProxyConf struct { - BaseProxyConf - DomainConf - - Multiplexer string `json:"multiplexer"` +func (cfg *TCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { + return nil } +// TCPMux func (cfg *TCPMuxProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*TCPMuxProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.DomainConf.compare(&cmpConf.DomainConf) || - cfg.Multiplexer != cmpConf.Multiplexer { + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { return false } + + // Add custom logic equal if exists. + if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { + return false + } + + if cfg.Multiplexer != cmpConf.Multiplexer { + return false + } + return true } +func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) - cfg.DomainConf.UnmarshalFromMsg(pMsg) + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists + cfg.CustomDomains = pMsg.CustomDomains + cfg.SubDomain = pMsg.SubDomain cfg.Multiplexer = pMsg.Multiplexer } -func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - - cfg.Multiplexer = section["multiplexer"] - if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { - return fmt.Errorf("parse conf error: proxy [%s] incorrect multiplexer [%s]", name, cfg.Multiplexer) - } - return -} - func (cfg *TCPMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - cfg.DomainConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists + pMsg.CustomDomains = cfg.CustomDomains + pMsg.SubDomain = cfg.SubDomain pMsg.Multiplexer = cfg.Multiplexer } func (cfg *TCPMuxProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return err + return } + + // Add custom logic check if exists if err = cfg.DomainConf.checkForCli(); err != nil { - return err + return } + if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer) } + return } @@ -650,101 +643,113 @@ func (cfg *TCPMuxProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) return } + return } // UDP -type UDPProxyConf struct { - BaseProxyConf - BindInfoConf -} - func (cfg *UDPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*UDPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) { + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { return false } + + // Add custom logic equal if exists. + if cfg.RemotePort != cmpConf.RemotePort { + return false + } + return true } -func (cfg *UDPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) - cfg.BindInfoConf.UnmarshalFromMsg(pMsg) +func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil } -func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - if err = cfg.BindInfoConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return +func (cfg *UDPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists + cfg.RemotePort = pMsg.RemotePort } func (cfg *UDPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - cfg.BindInfoConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists + pMsg.RemotePort = cfg.RemotePort } func (cfg *UDPProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { return } + + // Add custom logic check if exists + return } -func (cfg *UDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { return nil } - -// HTTP -type HTTPProxyConf struct { - BaseProxyConf - DomainConf - - Locations []string `json:"locations"` - HTTPUser string `json:"http_user"` - HTTPPwd string `json:"http_pwd"` - HostHeaderRewrite string `json:"host_header_rewrite"` - Headers map[string]string `json:"headers"` +func (cfg *UDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { + return nil } +// HTTP func (cfg *HTTPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*HTTPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.DomainConf.compare(&cmpConf.DomainConf) || - strings.Join(cfg.Locations, " ") != strings.Join(cmpConf.Locations, " ") || - cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite || - cfg.HTTPUser != cmpConf.HTTPUser || - cfg.HTTPPwd != cmpConf.HTTPPwd || - len(cfg.Headers) != len(cmpConf.Headers) { + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { return false } - for k, v := range cfg.Headers { - v2, ok := cmpConf.Headers[k] - if !ok { - return false - } - if v != v2 { - return false - } + // Add custom logic equal if exists. + if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { + return false } + + if !reflect.DeepEqual(cfg.Locations, cmpConf.Locations) || + cfg.HTTPUser != cmpConf.HTTPUser || + cfg.HTTPPwd != cmpConf.HTTPPwd || + cfg.HostHeaderRewrite != cmpConf.HostHeaderRewrite || + !reflect.DeepEqual(cfg.Headers, cmpConf.Headers) { + return false + } + return true } -func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) - cfg.DomainConf.UnmarshalFromMsg(pMsg) +func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + // Add custom logic unmarshal if exists + cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_") + + return nil +} + +func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists + cfg.CustomDomains = pMsg.CustomDomains + cfg.SubDomain = pMsg.SubDomain cfg.Locations = pMsg.Locations cfg.HostHeaderRewrite = pMsg.HostHeaderRewrite cfg.HTTPUser = pMsg.HTTPUser @@ -752,41 +757,12 @@ func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { cfg.Headers = pMsg.Headers } -func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - - var ( - tmpStr string - ok bool - ) - if tmpStr, ok = section["locations"]; ok { - cfg.Locations = strings.Split(tmpStr, ",") - } else { - cfg.Locations = []string{""} - } - - cfg.HostHeaderRewrite = section["host_header_rewrite"] - cfg.HTTPUser = section["http_user"] - cfg.HTTPPwd = section["http_pwd"] - cfg.Headers = make(map[string]string) - - for k, v := range section { - if strings.HasPrefix(k, "header_") { - cfg.Headers[strings.TrimPrefix(k, "header_")] = v - } - } - return -} - func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - cfg.DomainConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + // Add custom logic marshal if exists + pMsg.CustomDomains = cfg.CustomDomains + pMsg.SubDomain = cfg.SubDomain pMsg.Locations = cfg.Locations pMsg.HostHeaderRewrite = cfg.HostHeaderRewrite pMsg.HTTPUser = cfg.HTTPUser @@ -798,9 +774,12 @@ func (cfg *HTTPProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { return } + + // Add custom logic check if exists if err = cfg.DomainConf.checkForCli(); err != nil { return } + return } @@ -808,59 +787,71 @@ func (cfg *HTTPProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { if serverCfg.VhostHTTPPort == 0 { return fmt.Errorf("type [http] not support when vhost_http_port is not set") } + if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) return } + return } // HTTPS -type HTTPSProxyConf struct { - BaseProxyConf - DomainConf -} - func (cfg *HTTPSProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*HTTPSProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.DomainConf.compare(&cmpConf.DomainConf) { + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { return false } + + // Add custom logic equal if exists. + if !reflect.DeepEqual(cfg.DomainConf, cmpConf.DomainConf) { + return false + } + return true } -func (cfg *HTTPSProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) - cfg.DomainConf.UnmarshalFromMsg(pMsg) +func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil } -func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return +func (cfg *HTTPSProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists + cfg.CustomDomains = pMsg.CustomDomains + cfg.SubDomain = pMsg.SubDomain } func (cfg *HTTPSProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - cfg.DomainConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists + pMsg.CustomDomains = cfg.CustomDomains + pMsg.SubDomain = cfg.SubDomain } func (cfg *HTTPSProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { return } + + // Add custom logic check if exists if err = cfg.DomainConf.checkForCli(); err != nil { return } + return } @@ -868,127 +859,124 @@ func (cfg *HTTPSProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { if serverCfg.VhostHTTPSPort == 0 { return fmt.Errorf("type [https] not support when vhost_https_port is not set") } + if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) return } + return } // SUDP -type SUDPProxyConf struct { - BaseProxyConf - - Role string `json:"role"` - Sk string `json:"sk"` -} - func (cfg *SUDPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*SUDPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - cfg.Role != cmpConf.Role || + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { + return false + } + + // Add custom logic equal if exists. + if cfg.Role != cmpConf.Role || cfg.Sk != cmpConf.Sk { return false } + return true } -func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return +func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err } - cfg.Role = section["role"] - if cfg.Role != "server" { - return fmt.Errorf("Parse conf error: proxy [%s] incorrect role [%s]", name, cfg.Role) - } + // Add custom logic unmarshal if exists - cfg.Sk = section["sk"] - - if err = cfg.LocalSvrConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return -} - -func (cfg *SUDPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) - pMsg.Sk = cfg.Sk -} - -func (cfg *SUDPProxyConf) CheckForCli() (err error) { - if err = cfg.BaseProxyConf.checkForCli(); err != nil { - return - } - if cfg.Role != "server" { - err = fmt.Errorf("role should be 'server'") - return - } - return -} - -func (cfg *SUDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - return + return nil } // Only for role server. func (cfg *SUDPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists cfg.Sk = pMsg.Sk } -// STCP -type STCPProxyConf struct { - BaseProxyConf +func (cfg *SUDPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.marshalToMsg(pMsg) - Role string `json:"role"` - Sk string `json:"sk"` + // Add custom logic marshal if exists + pMsg.Sk = cfg.Sk } +func (cfg *SUDPProxyConf) CheckForCli() (err error) { + if err := cfg.BaseProxyConf.checkForCli(); err != nil { + return err + } + + // Add custom logic check if exists + if cfg.Role != "server" { + return fmt.Errorf("role should be 'server'") + } + + return nil +} + +func (cfg *SUDPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { + return nil +} + +// STCP func (cfg *STCPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*STCPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - cfg.Role != cmpConf.Role || + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { + return false + } + + // Add custom logic equal if exists. + if cfg.Role != cmpConf.Role || cfg.Sk != cmpConf.Sk { return false } + return true } +func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + + return nil +} + // Only for role server. func (cfg *STCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists cfg.Sk = pMsg.Sk } -func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - - cfg.Role = section["role"] - if cfg.Role != "server" { - return fmt.Errorf("Parse conf error: proxy [%s] incorrect role [%s]", name, cfg.Role) - } - - cfg.Sk = section["sk"] - - if err = cfg.LocalSvrConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return -} - func (cfg *STCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists pMsg.Sk = cfg.Sk } @@ -996,66 +984,65 @@ func (cfg *STCPProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { return } + + // Add custom logic check if exists if cfg.Role != "server" { - err = fmt.Errorf("role should be 'server'") - return + return fmt.Errorf("role should be 'server'") } + return } -func (cfg *STCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - return +func (cfg *STCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { + return nil } // XTCP -type XTCPProxyConf struct { - BaseProxyConf - - Role string `json:"role"` - Sk string `json:"sk"` -} - func (cfg *XTCPProxyConf) Compare(cmp ProxyConf) bool { cmpConf, ok := cmp.(*XTCPProxyConf) if !ok { return false } - if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || - !cfg.LocalSvrConf.compare(&cmpConf.LocalSvrConf) || - cfg.Role != cmpConf.Role || + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) { + return false + } + + // Add custom logic equal if exists. + if cfg.Role != cmpConf.Role || cfg.Sk != cmpConf.Sk { return false } + return true } +func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + + return nil +} + // Only for role server. func (cfg *XTCPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) + cfg.BaseProxyConf.unmarshalFromMsg(pMsg) + + // Add custom logic unmarshal if exists cfg.Sk = pMsg.Sk } -func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - - cfg.Role = section["role"] - if cfg.Role != "server" { - return fmt.Errorf("Parse conf error: proxy [%s] incorrect role [%s]", name, cfg.Role) - } - - cfg.Sk = section["sk"] - - if err = cfg.LocalSvrConf.UnmarshalFromIni(prefix, name, section); err != nil { - return - } - return -} - func (cfg *XTCPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { - cfg.BaseProxyConf.MarshalToMsg(pMsg) + cfg.BaseProxyConf.marshalToMsg(pMsg) + + // Add custom logic marshal if exists pMsg.Sk = cfg.Sk } @@ -1063,125 +1050,15 @@ func (cfg *XTCPProxyConf) CheckForCli() (err error) { if err = cfg.BaseProxyConf.checkForCli(); err != nil { return } + + // Add custom logic check if exists if cfg.Role != "server" { - err = fmt.Errorf("role should be 'server'") - return + return fmt.Errorf("role should be 'server'") } + return } -func (cfg *XTCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { - return -} - -func ParseRangeSection(name string, section ini.Section) (sections map[string]ini.Section, err error) { - localPorts, errRet := util.ParseRangeNumbers(section["local_port"]) - if errRet != nil { - err = fmt.Errorf("Parse conf error: range section [%s] local_port invalid, %v", name, errRet) - return - } - - remotePorts, errRet := util.ParseRangeNumbers(section["remote_port"]) - if errRet != nil { - err = fmt.Errorf("Parse conf error: range section [%s] remote_port invalid, %v", name, errRet) - return - } - if len(localPorts) != len(remotePorts) { - err = fmt.Errorf("Parse conf error: range section [%s] local ports number should be same with remote ports number", name) - return - } - if len(localPorts) == 0 { - err = fmt.Errorf("Parse conf error: range section [%s] local_port and remote_port is necessary", name) - return - } - - sections = make(map[string]ini.Section) - for i, port := range localPorts { - subName := fmt.Sprintf("%s_%d", name, i) - subSection := copySection(section) - subSection["local_port"] = fmt.Sprintf("%d", port) - subSection["remote_port"] = fmt.Sprintf("%d", remotePorts[i]) - sections[subName] = subSection - } - return -} - -// if len(startProxy) is 0, start all -// otherwise just start proxies in startProxy map -func LoadAllConfFromIni(prefix string, content string, startProxy map[string]struct{}) ( - proxyConfs map[string]ProxyConf, visitorConfs map[string]VisitorConf, err error) { - - conf, errRet := ini.Load(strings.NewReader(content)) - if errRet != nil { - err = errRet - return - } - - if prefix != "" { - prefix += "." - } - - startAll := true - if len(startProxy) > 0 { - startAll = false - } - proxyConfs = make(map[string]ProxyConf) - visitorConfs = make(map[string]VisitorConf) - for name, section := range conf { - if name == "common" { - continue - } - - _, shouldStart := startProxy[name] - if !startAll && !shouldStart { - continue - } - - subSections := make(map[string]ini.Section) - - if strings.HasPrefix(name, "range:") { - // range section - rangePrefix := strings.TrimSpace(strings.TrimPrefix(name, "range:")) - subSections, err = ParseRangeSection(rangePrefix, section) - if err != nil { - return - } - } else { - subSections[name] = section - } - - for subName, subSection := range subSections { - if subSection["role"] == "" { - subSection["role"] = "server" - } - role := subSection["role"] - if role == "server" { - cfg, errRet := NewProxyConfFromIni(prefix, subName, subSection) - if errRet != nil { - err = errRet - return - } - proxyConfs[prefix+subName] = cfg - } else if role == "visitor" { - cfg, errRet := NewVisitorConfFromIni(prefix, subName, subSection) - if errRet != nil { - err = errRet - return - } - visitorConfs[prefix+subName] = cfg - } else { - err = fmt.Errorf("role should be 'server' or 'visitor'") - return - } - } - } - return -} - -func copySection(section ini.Section) (out ini.Section) { - out = make(ini.Section) - for k, v := range section { - out[k] = v - } - return +func (cfg *XTCPProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { + return nil } diff --git a/pkg/config/proxy_test.go b/pkg/config/proxy_test.go new file mode 100644 index 00000000..68c87b95 --- /dev/null +++ b/pkg/config/proxy_test.go @@ -0,0 +1,461 @@ +// Copyright 2020 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 config + +import ( + "testing" + + "github.com/fatedier/frp/pkg/consts" + "github.com/stretchr/testify/assert" + + "gopkg.in/ini.v1" +) + +var ( + testLoadOptions = ini.LoadOptions{ + Insensitive: false, + InsensitiveSections: false, + InsensitiveKeys: false, + IgnoreInlineComment: true, + AllowBooleanKeys: true, + } + + testProxyPrefix = "test." +) + +func Test_Proxy_Interface(t *testing.T) { + for name := range proxyConfTypeMap { + NewConfByType(name) + } +} + +func Test_Proxy_UnmarshalFromIni(t *testing.T) { + assert := assert.New(t) + + testcases := []struct { + sname string + source []byte + expected ProxyConf + }{ + + { + sname: "ssh", + source: []byte(` + [ssh] + # tcp | udp | http | https | stcp | xtcp, default is tcp + type = tcp + local_ip = 127.0.0.9 + local_port = 29 + bandwidth_limit = 19MB + use_encryption + use_compression + remote_port = 6009 + group = test_group + group_key = 123456 + health_check_type = tcp + health_check_timeout_s = 3 + health_check_max_failed = 3 + health_check_interval_s = 19 + meta_var1 = 123 + meta_var2 = 234`), + expected: &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "ssh", + ProxyType: consts.TCPProxy, + UseCompression: true, + UseEncryption: true, + Group: "test_group", + GroupKey: "123456", + BandwidthLimit: MustBandwidthQuantity("19MB"), + Metas: map[string]string{ + "var1": "123", + "var2": "234", + }, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 29, + }, + HealthCheckConf: HealthCheckConf{ + HealthCheckType: consts.TCPProxy, + HealthCheckTimeoutS: 3, + HealthCheckMaxFailed: 3, + HealthCheckIntervalS: 19, + HealthCheckAddr: "127.0.0.9:29", + }, + }, + RemotePort: 6009, + }, + }, + { + sname: "ssh_random", + source: []byte(` + [ssh_random] + type = tcp + local_ip = 127.0.0.9 + local_port = 29 + remote_port = 9 + `), + expected: &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "ssh_random", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 29, + }, + }, + RemotePort: 9, + }, + }, + { + sname: "dns", + source: []byte(` + [dns] + type = udp + local_ip = 114.114.114.114 + local_port = 59 + remote_port = 6009 + use_encryption + use_compression + `), + expected: &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "dns", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 59, + }, + }, + RemotePort: 6009, + }, + }, + { + sname: "web01", + source: []byte(` + [web01] + type = http + local_ip = 127.0.0.9 + local_port = 89 + use_encryption + use_compression + http_user = admin + http_pwd = admin + subdomain = web01 + custom_domains = web02.yourdomain.com + locations = /,/pic + host_header_rewrite = example.com + header_X-From-Where = frp + health_check_type = http + health_check_url = /status + health_check_interval_s = 19 + health_check_max_failed = 3 + health_check_timeout_s = 3 + `), + expected: &HTTPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "web01", + ProxyType: consts.HTTPProxy, + UseCompression: true, + UseEncryption: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 89, + }, + HealthCheckConf: HealthCheckConf{ + HealthCheckType: consts.HTTPProxy, + HealthCheckTimeoutS: 3, + HealthCheckMaxFailed: 3, + HealthCheckIntervalS: 19, + HealthCheckURL: "http://127.0.0.9:89/status", + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"web02.yourdomain.com"}, + SubDomain: "web01", + }, + Locations: []string{"/", "/pic"}, + HTTPUser: "admin", + HTTPPwd: "admin", + HostHeaderRewrite: "example.com", + Headers: map[string]string{ + "X-From-Where": "frp", + }, + }, + }, + { + sname: "web02", + source: []byte(` + [web02] + type = https + local_ip = 127.0.0.9 + local_port = 8009 + use_encryption + use_compression + subdomain = web01 + custom_domains = web02.yourdomain.com + proxy_protocol_version = v2 + `), + expected: &HTTPSProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "web02", + ProxyType: consts.HTTPSProxy, + UseCompression: true, + UseEncryption: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 8009, + }, + ProxyProtocolVersion: "v2", + }, + DomainConf: DomainConf{ + CustomDomains: []string{"web02.yourdomain.com"}, + SubDomain: "web01", + }, + }, + }, + { + sname: "secret_tcp", + source: []byte(` + [secret_tcp] + type = stcp + sk = abcdefg + local_ip = 127.0.0.1 + local_port = 22 + use_encryption = false + use_compression = false + `), + expected: &STCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "secret_tcp", + ProxyType: consts.STCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 22, + }, + }, + Role: "server", + Sk: "abcdefg", + }, + }, + { + sname: "p2p_tcp", + source: []byte(` + [p2p_tcp] + type = xtcp + sk = abcdefg + local_ip = 127.0.0.1 + local_port = 22 + use_encryption = false + use_compression = false + `), + expected: &XTCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "p2p_tcp", + ProxyType: consts.XTCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 22, + }, + }, + Role: "server", + Sk: "abcdefg", + }, + }, + { + sname: "tcpmuxhttpconnect", + source: []byte(` + [tcpmuxhttpconnect] + type = tcpmux + multiplexer = httpconnect + local_ip = 127.0.0.1 + local_port = 10701 + custom_domains = tunnel1 + `), + expected: &TCPMuxProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "tcpmuxhttpconnect", + ProxyType: consts.TCPMuxProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.1", + LocalPort: 10701, + }, + }, + DomainConf: DomainConf{ + CustomDomains: []string{"tunnel1"}, + SubDomain: "", + }, + Multiplexer: "httpconnect", + }, + }, + } + + for _, c := range testcases { + f, err := ini.LoadSources(testLoadOptions, c.source) + assert.NoError(err) + + proxyType := f.Section(c.sname).Key("type").String() + assert.NotEmpty(proxyType) + + actual := DefaultProxyConf(proxyType) + assert.NotNil(actual) + + err = actual.UnmarshalFromIni(testProxyPrefix, c.sname, f.Section(c.sname)) + assert.NoError(err) + assert.Equal(c.expected, actual) + } +} + +func Test_RangeProxy_UnmarshalFromIni(t *testing.T) { + assert := assert.New(t) + + testcases := []struct { + sname string + source []byte + expected map[string]ProxyConf + }{ + { + sname: "range:tcp_port", + source: []byte(` + [range:tcp_port] + type = tcp + local_ip = 127.0.0.9 + local_port = 6010-6011,6019 + remote_port = 6010-6011,6019 + use_encryption = false + use_compression = false + `), + expected: map[string]ProxyConf{ + "tcp_port_0": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "tcp_port_0", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6010, + }, + }, + RemotePort: 6010, + }, + "tcp_port_1": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "tcp_port_1", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6011, + }, + }, + RemotePort: 6011, + }, + "tcp_port_2": &TCPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "tcp_port_2", + ProxyType: consts.TCPProxy, + LocalSvrConf: LocalSvrConf{ + LocalIP: "127.0.0.9", + LocalPort: 6019, + }, + }, + RemotePort: 6019, + }, + }, + }, + { + sname: "range:udp_port", + source: []byte(` + [range:udp_port] + type = udp + local_ip = 114.114.114.114 + local_port = 6000,6010-6011 + remote_port = 6000,6010-6011 + use_encryption + use_compression + `), + expected: map[string]ProxyConf{ + "udp_port_0": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "udp_port_0", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6000, + }, + }, + RemotePort: 6000, + }, + "udp_port_1": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "udp_port_1", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6010, + }, + }, + RemotePort: 6010, + }, + "udp_port_2": &UDPProxyConf{ + BaseProxyConf: BaseProxyConf{ + ProxyName: testProxyPrefix + "udp_port_2", + ProxyType: consts.UDPProxy, + UseEncryption: true, + UseCompression: true, + LocalSvrConf: LocalSvrConf{ + LocalIP: "114.114.114.114", + LocalPort: 6011, + }, + }, + RemotePort: 6011, + }, + }, + }, + } + + for _, c := range testcases { + + f, err := ini.LoadSources(testLoadOptions, c.source) + assert.NoError(err) + + actual := make(map[string]ProxyConf) + s := f.Section(c.sname) + + err = renderRangeProxyTemplates(f, s) + assert.NoError(err) + + f.DeleteSection(ini.DefaultSection) + f.DeleteSection(c.sname) + + for _, section := range f.Sections() { + proxyType := section.Key("type").String() + newsname := section.Name() + + tmp := DefaultProxyConf(proxyType) + err = tmp.UnmarshalFromIni(testProxyPrefix, newsname, section) + assert.NoError(err) + + actual[newsname] = tmp + } + + assert.Equal(c.expected, actual) + } + +} diff --git a/pkg/config/server.go b/pkg/config/server.go new file mode 100644 index 00000000..10d5b33d --- /dev/null +++ b/pkg/config/server.go @@ -0,0 +1,284 @@ +// Copyright 2020 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 config + +import ( + "fmt" + "strings" + + "github.com/fatedier/frp/pkg/auth" + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/util/util" + + "gopkg.in/ini.v1" +) + +// ServerCommonConf contains information for a server service. It is +// recommended to use GetDefaultServerConf instead of creating this object +// directly, so that all unspecified fields have reasonable default values. +type ServerCommonConf struct { + auth.ServerConfig `ini:",extends" json:"inline"` + + // BindAddr specifies the address that the server binds to. By default, + // this value is "0.0.0.0". + BindAddr string `ini:"bind_addr" json:"bind_addr"` + // BindPort specifies the port that the server listens on. By default, this + // value is 7000. + BindPort int `ini:"bind_port" json:"bind_port"` + // BindUDPPort specifies the UDP port that the server listens on. If this + // value is 0, the server will not listen for UDP connections. By default, + // this value is 0 + BindUDPPort int `ini:"bind_udp_port" json:"bind_udp_port"` + // KCPBindPort specifies the KCP port that the server listens on. If this + // value is 0, the server will not listen for KCP connections. By default, + // this value is 0. + KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port"` + // ProxyBindAddr specifies the address that the proxy binds to. This value + // may be the same as BindAddr. By default, this value is "0.0.0.0". + ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"` + // VhostHTTPPort specifies the port that the server listens for HTTP Vhost + // requests. If this value is 0, the server will not listen for HTTP + // requests. By default, this value is 0. + VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"` + // VhostHTTPSPort specifies the port that the server listens for HTTPS + // Vhost requests. If this value is 0, the server will not listen for HTTPS + // requests. By default, this value is 0. + VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port"` + // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP + // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP + // requests on one single port. If it's not - it will listen on this value for + // HTTP CONNECT requests. By default, this value is 0. + TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port"` + // VhostHTTPTimeout specifies the response header timeout for the Vhost + // HTTP server, in seconds. By default, this value is 60. + VhostHTTPTimeout int64 `ini:"vhost_http_timeout" json:"vhost_http_timeout"` + // DashboardAddr specifies the address that the dashboard binds to. By + // default, this value is "0.0.0.0". + DashboardAddr string `ini:"dashboard_addr" json:"dashboard_addr"` + // DashboardPort specifies the port that the dashboard listens on. If this + // value is 0, the dashboard will not be started. By default, this value is + // 0. + DashboardPort int `ini:"dashboard_port" json:"dashboard_port"` + // DashboardUser specifies the username that the dashboard will use for + // login. By default, this value is "admin". + DashboardUser string `ini:"dashboard_user" json:"dashboard_user"` + // DashboardUser specifies the password that the dashboard will use for + // login. By default, this value is "admin". + DashboardPwd string `ini:"dashboard_pwd" json:"dashboard_pwd"` + // EnablePrometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} + // in /metrics api. + EnablePrometheus bool `ini:"enable_prometheus" json:"enable_prometheus"` + // AssetsDir specifies the local directory that the dashboard will load + // resources from. If this value is "", assets will be loaded from the + // bundled executable using statik. By default, this value is "". + AssetsDir string `ini:"assets_dir" json:"assets_dir"` + // LogFile specifies a file where logs will be written to. This value will + // only be used if LogWay is set appropriately. By default, this value is + // "console". + LogFile string `ini:"log_file" json:"log_file"` + // LogWay specifies the way logging is managed. Valid values are "console" + // or "file". If "console" is used, logs will be printed to stdout. If + // "file" is used, logs will be printed to LogFile. By default, this value + // is "console". + LogWay string `ini:"log_way" json:"log_way"` + // LogLevel specifies the minimum log level. Valid values are "trace", + // "debug", "info", "warn", and "error". By default, this value is "info". + LogLevel string `ini:"log_level" json:"log_level"` + // LogMaxDays specifies the maximum number of days to store log information + // before deletion. This is only used if LogWay == "file". By default, this + // value is 0. + LogMaxDays int64 `ini:"log_max_days" json:"log_max_days"` + // DisableLogColor disables log colors when LogWay == "console" when set to + // true. By default, this value is false. + DisableLogColor bool `ini:"disable_log_color" json:"disable_log_color"` + // DetailedErrorsToClient defines whether to send the specific error (with + // debug info) to frpc. By default, this value is true. + DetailedErrorsToClient bool `ini:"detailed_errors_to_client" json:"detailed_errors_to_client"` + + // SubDomainHost specifies the domain that will be attached to sub-domains + // requested by the client when using Vhost proxying. For example, if this + // value is set to "frps.com" and the client requested the subdomain + // "test", the resulting URL would be "test.frps.com". By default, this + // value is "". + SubDomainHost string `ini:"subdomain_host" json:"subdomain_host"` + // TCPMux toggles TCP stream multiplexing. This allows multiple requests + // from a client to share a single TCP connection. By default, this value + // is true. + TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` + // Custom404Page specifies a path to a custom 404 page to display. If this + // value is "", a default page will be displayed. By default, this value is + // "". + Custom404Page string `ini:"custom_404_page" json:"custom_404_page"` + + // AllowPorts specifies a set of ports that clients are able to proxy to. + // If the length of this value is 0, all ports are allowed. By default, + // this value is an empty set. + AllowPorts map[int]struct{} `ini:"-" json:"-"` + // MaxPoolCount specifies the maximum pool size for each proxy. By default, + // this value is 5. + MaxPoolCount int64 `ini:"max_pool_count" json:"max_pool_count"` + // MaxPortsPerClient specifies the maximum number of ports a single client + // may proxy to. If this value is 0, no limit will be applied. By default, + // this value is 0. + MaxPortsPerClient int64 `ini:"max_ports_per_client" json:"max_ports_per_client"` + // TLSOnly specifies whether to only accept TLS-encrypted connections. + // By default, the value is false. + TLSOnly bool `ini:"tls_only" json:"tls_only"` + // TLSCertFile specifies the path of the cert file that the server will + // load. If "tls_cert_file", "tls_key_file" are valid, the server will use this + // supplied tls configuration. Otherwise, the server will use the tls + // configuration generated by itself. + TLSCertFile string `ini:"tls_cert_file" json:"tls_cert_file"` + // TLSKeyFile specifies the path of the secret key that the server will + // load. If "tls_cert_file", "tls_key_file" are valid, the server will use this + // supplied tls configuration. Otherwise, the server will use the tls + // configuration generated by itself. + TLSKeyFile string `ini:"tls_key_file" json:"tls_key_file"` + // TLSTrustedCaFile specifies the paths of the client cert files that the + // server will load. It only works when "tls_only" is true. If + // "tls_trusted_ca_file" is valid, the server will verify each client's + // certificate. + TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"` + // HeartBeatTimeout specifies the maximum time to wait for a heartbeat + // before terminating the connection. It is not recommended to change this + // value. By default, this value is 90. + HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` + // UserConnTimeout specifies the maximum time to wait for a work + // connection. By default, this value is 10. + UserConnTimeout int64 `ini:"user_conn_timeout" json:"user_conn_timeout"` + // HTTPPlugins specify the server plugins support HTTP protocol. + HTTPPlugins map[string]plugin.HTTPPluginOptions `ini:"-" json:"http_plugins"` + // UDPPacketSize specifies the UDP packet size + // By default, this value is 1500 + UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` +} + +// GetDefaultServerConf returns a server configuration with reasonable +// defaults. +func GetDefaultServerConf() ServerCommonConf { + return ServerCommonConf{ + ServerConfig: auth.GetDefaultServerConf(), + BindAddr: "0.0.0.0", + BindPort: 7000, + BindUDPPort: 0, + KCPBindPort: 0, + ProxyBindAddr: "0.0.0.0", + VhostHTTPPort: 0, + VhostHTTPSPort: 0, + TCPMuxHTTPConnectPort: 0, + VhostHTTPTimeout: 60, + DashboardAddr: "0.0.0.0", + DashboardPort: 0, + DashboardUser: "admin", + DashboardPwd: "admin", + EnablePrometheus: false, + AssetsDir: "", + LogFile: "console", + LogWay: "console", + LogLevel: "info", + LogMaxDays: 3, + DisableLogColor: false, + DetailedErrorsToClient: true, + SubDomainHost: "", + TCPMux: true, + AllowPorts: make(map[int]struct{}), + MaxPoolCount: 5, + MaxPortsPerClient: 0, + TLSOnly: false, + TLSCertFile: "", + TLSKeyFile: "", + TLSTrustedCaFile: "", + HeartbeatTimeout: 90, + UserConnTimeout: 10, + Custom404Page: "", + HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), + UDPPacketSize: 1500, + } +} + +func (cfg *ServerCommonConf) Check() error { + return nil +} + +func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { + + f, err := ini.LoadSources(ini.LoadOptions{ + Insensitive: false, + InsensitiveSections: false, + InsensitiveKeys: false, + IgnoreInlineComment: true, + AllowBooleanKeys: true, + }, source) + if err != nil { + return ServerCommonConf{}, err + } + + s, err := f.GetSection("common") + if err != nil { + // TODO: add error info + return ServerCommonConf{}, err + } + + common := GetDefaultServerConf() + err = s.MapTo(&common) + if err != nil { + return ServerCommonConf{}, err + } + + // allow_ports + allowPortStr := s.Key("allow_ports").String() + if allowPortStr != "" { + allowPorts, err := util.ParseRangeNumbers(allowPortStr) + if err != nil { + return ServerCommonConf{}, fmt.Errorf("Parse conf error: allow_ports: %v", err) + } + for _, port := range allowPorts { + common.AllowPorts[int(port)] = struct{}{} + } + } + + // plugin.xxx + pluginOpts := make(map[string]plugin.HTTPPluginOptions) + for _, section := range f.Sections() { + name := section.Name() + if !strings.HasPrefix(name, "plugin.") { + continue + } + + opt, err := loadHTTPPluginOpt(section) + if err != nil { + return ServerCommonConf{}, err + } + + pluginOpts[opt.Name] = *opt + } + common.HTTPPlugins = pluginOpts + + return common, nil +} + +func loadHTTPPluginOpt(section *ini.Section) (*plugin.HTTPPluginOptions, error) { + name := strings.TrimSpace(strings.TrimPrefix(section.Name(), "plugin.")) + + opt := new(plugin.HTTPPluginOptions) + err := section.MapTo(opt) + if err != nil { + return nil, err + } + + opt.Name = name + + return opt, nil +} diff --git a/pkg/config/server_common.go b/pkg/config/server_common.go deleted file mode 100644 index e9be2081..00000000 --- a/pkg/config/server_common.go +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright 2016 fatedier, fatedier@gmail.com -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// 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 config - -import ( - "fmt" - "strconv" - "strings" - - "github.com/fatedier/frp/pkg/auth" - plugin "github.com/fatedier/frp/pkg/plugin/server" - "github.com/fatedier/frp/pkg/util/util" - - ini "github.com/vaughan0/go-ini" -) - -// ServerCommonConf contains information for a server service. It is -// recommended to use GetDefaultServerConf instead of creating this object -// directly, so that all unspecified fields have reasonable default values. -type ServerCommonConf struct { - auth.ServerConfig - // BindAddr specifies the address that the server binds to. By default, - // this value is "0.0.0.0". - BindAddr string `json:"bind_addr"` - // BindPort specifies the port that the server listens on. By default, this - // value is 7000. - BindPort int `json:"bind_port"` - // BindUDPPort specifies the UDP port that the server listens on. If this - // value is 0, the server will not listen for UDP connections. By default, - // this value is 0 - BindUDPPort int `json:"bind_udp_port"` - // KCPBindPort specifies the KCP port that the server listens on. If this - // value is 0, the server will not listen for KCP connections. By default, - // this value is 0. - KCPBindPort int `json:"kcp_bind_port"` - // ProxyBindAddr specifies the address that the proxy binds to. This value - // may be the same as BindAddr. By default, this value is "0.0.0.0". - ProxyBindAddr string `json:"proxy_bind_addr"` - // VhostHTTPPort specifies the port that the server listens for HTTP Vhost - // requests. If this value is 0, the server will not listen for HTTP - // requests. By default, this value is 0. - VhostHTTPPort int `json:"vhost_http_port"` - // VhostHTTPSPort specifies the port that the server listens for HTTPS - // Vhost requests. If this value is 0, the server will not listen for HTTPS - // requests. By default, this value is 0. - VhostHTTPSPort int `json:"vhost_https_port"` - // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP - // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP - // requests on one single port. If it's not - it will listen on this value for - // HTTP CONNECT requests. By default, this value is 0. - TCPMuxHTTPConnectPort int `json:"tcpmux_httpconnect_port"` - // VhostHTTPTimeout specifies the response header timeout for the Vhost - // HTTP server, in seconds. By default, this value is 60. - VhostHTTPTimeout int64 `json:"vhost_http_timeout"` - // DashboardAddr specifies the address that the dashboard binds to. By - // default, this value is "0.0.0.0". - DashboardAddr string `json:"dashboard_addr"` - // DashboardPort specifies the port that the dashboard listens on. If this - // value is 0, the dashboard will not be started. By default, this value is - // 0. - DashboardPort int `json:"dashboard_port"` - // DashboardUser specifies the username that the dashboard will use for - // login. By default, this value is "admin". - DashboardUser string `json:"dashboard_user"` - // DashboardUser specifies the password that the dashboard will use for - // login. By default, this value is "admin". - DashboardPwd string `json:"dashboard_pwd"` - // EnablePrometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port} - // in /metrics api. - EnablePrometheus bool `json:"enable_prometheus"` - // AssetsDir specifies the local directory that the dashboard will load - // resources from. If this value is "", assets will be loaded from the - // bundled executable using statik. By default, this value is "". - AssetsDir string `json:"assets_dir"` - // LogFile specifies a file where logs will be written to. This value will - // only be used if LogWay is set appropriately. By default, this value is - // "console". - LogFile string `json:"log_file"` - // LogWay specifies the way logging is managed. Valid values are "console" - // or "file". If "console" is used, logs will be printed to stdout. If - // "file" is used, logs will be printed to LogFile. By default, this value - // is "console". - LogWay string `json:"log_way"` - // LogLevel specifies the minimum log level. Valid values are "trace", - // "debug", "info", "warn", and "error". By default, this value is "info". - LogLevel string `json:"log_level"` - // LogMaxDays specifies the maximum number of days to store log information - // before deletion. This is only used if LogWay == "file". By default, this - // value is 0. - LogMaxDays int64 `json:"log_max_days"` - // DisableLogColor disables log colors when LogWay == "console" when set to - // true. By default, this value is false. - DisableLogColor bool `json:"disable_log_color"` - // DetailedErrorsToClient defines whether to send the specific error (with - // debug info) to frpc. By default, this value is true. - DetailedErrorsToClient bool `json:"detailed_errors_to_client"` - - // SubDomainHost specifies the domain that will be attached to sub-domains - // requested by the client when using Vhost proxying. For example, if this - // value is set to "frps.com" and the client requested the subdomain - // "test", the resulting URL would be "test.frps.com". By default, this - // value is "". - SubDomainHost string `json:"subdomain_host"` - // TCPMux toggles TCP stream multiplexing. This allows multiple requests - // from a client to share a single TCP connection. By default, this value - // is true. - TCPMux bool `json:"tcp_mux"` - // Custom404Page specifies a path to a custom 404 page to display. If this - // value is "", a default page will be displayed. By default, this value is - // "". - Custom404Page string `json:"custom_404_page"` - - // AllowPorts specifies a set of ports that clients are able to proxy to. - // If the length of this value is 0, all ports are allowed. By default, - // this value is an empty set. - AllowPorts map[int]struct{} - // MaxPoolCount specifies the maximum pool size for each proxy. By default, - // this value is 5. - MaxPoolCount int64 `json:"max_pool_count"` - // MaxPortsPerClient specifies the maximum number of ports a single client - // may proxy to. If this value is 0, no limit will be applied. By default, - // this value is 0. - MaxPortsPerClient int64 `json:"max_ports_per_client"` - // TLSOnly specifies whether to only accept TLS-encrypted connections. - // By default, the value is false. - TLSOnly bool `json:"tls_only"` - // TLSCertFile specifies the path of the cert file that the server will - // load. If "tls_cert_file", "tls_key_file" are valid, the server will use this - // supplied tls configuration. Otherwise, the server will use the tls - // configuration generated by itself. - TLSCertFile string `json:"tls_cert_file"` - // TLSKeyFile specifies the path of the secret key that the server will - // load. If "tls_cert_file", "tls_key_file" are valid, the server will use this - // supplied tls configuration. Otherwise, the server will use the tls - // configuration generated by itself. - TLSKeyFile string `json:"tls_key_file"` - // TLSTrustedCaFile specifies the paths of the client cert files that the - // server will load. It only works when "tls_only" is true. If - // "tls_trusted_ca_file" is valid, the server will verify each client's - // certificate. - TLSTrustedCaFile string `json:"tls_trusted_ca_file"` - // HeartBeatTimeout specifies the maximum time to wait for a heartbeat - // before terminating the connection. It is not recommended to change this - // value. By default, this value is 90. - HeartbeatTimeout int64 `json:"heartbeat_timeout"` - // UserConnTimeout specifies the maximum time to wait for a work - // connection. By default, this value is 10. - UserConnTimeout int64 `json:"user_conn_timeout"` - // HTTPPlugins specify the server plugins support HTTP protocol. - HTTPPlugins map[string]plugin.HTTPPluginOptions `json:"http_plugins"` - // UDPPacketSize specifies the UDP packet size - // By default, this value is 1500 - UDPPacketSize int64 `json:"udp_packet_size"` -} - -// GetDefaultServerConf returns a server configuration with reasonable -// defaults. -func GetDefaultServerConf() ServerCommonConf { - return ServerCommonConf{ - BindAddr: "0.0.0.0", - BindPort: 7000, - BindUDPPort: 0, - KCPBindPort: 0, - ProxyBindAddr: "0.0.0.0", - VhostHTTPPort: 0, - VhostHTTPSPort: 0, - TCPMuxHTTPConnectPort: 0, - VhostHTTPTimeout: 60, - DashboardAddr: "0.0.0.0", - DashboardPort: 0, - DashboardUser: "admin", - DashboardPwd: "admin", - EnablePrometheus: false, - AssetsDir: "", - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DisableLogColor: false, - DetailedErrorsToClient: true, - SubDomainHost: "", - TCPMux: true, - AllowPorts: make(map[int]struct{}), - MaxPoolCount: 5, - MaxPortsPerClient: 0, - TLSOnly: false, - TLSCertFile: "", - TLSKeyFile: "", - TLSTrustedCaFile: "", - HeartbeatTimeout: 90, - UserConnTimeout: 10, - Custom404Page: "", - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), - UDPPacketSize: 1500, - } -} - -// UnmarshalServerConfFromIni parses the contents of a server configuration ini -// file and returns the resulting server configuration. -func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error) { - cfg = GetDefaultServerConf() - - conf, err := ini.Load(strings.NewReader(content)) - if err != nil { - err = fmt.Errorf("parse ini conf file error: %v", err) - return ServerCommonConf{}, err - } - - UnmarshalPluginsFromIni(conf, &cfg) - - cfg.ServerConfig = auth.UnmarshalServerConfFromIni(conf) - - var ( - tmpStr string - ok bool - v int64 - ) - if tmpStr, ok = conf.Get("common", "bind_addr"); ok { - cfg.BindAddr = tmpStr - } - - if tmpStr, ok = conf.Get("common", "bind_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid bind_port") - return - } - cfg.BindPort = int(v) - } - - if tmpStr, ok = conf.Get("common", "bind_udp_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid bind_udp_port") - return - } - cfg.BindUDPPort = int(v) - } - - if tmpStr, ok = conf.Get("common", "kcp_bind_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid kcp_bind_port") - return - } - cfg.KCPBindPort = int(v) - } - - if tmpStr, ok = conf.Get("common", "proxy_bind_addr"); ok { - cfg.ProxyBindAddr = tmpStr - } else { - cfg.ProxyBindAddr = cfg.BindAddr - } - - if tmpStr, ok = conf.Get("common", "vhost_http_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid vhost_http_port") - return - } - cfg.VhostHTTPPort = int(v) - } else { - cfg.VhostHTTPPort = 0 - } - - if tmpStr, ok = conf.Get("common", "vhost_https_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid vhost_https_port") - return - } - cfg.VhostHTTPSPort = int(v) - } else { - cfg.VhostHTTPSPort = 0 - } - - if tmpStr, ok = conf.Get("common", "tcpmux_httpconnect_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid tcpmux_httpconnect_port") - return - } - cfg.TCPMuxHTTPConnectPort = int(v) - } else { - cfg.TCPMuxHTTPConnectPort = 0 - } - - if tmpStr, ok = conf.Get("common", "vhost_http_timeout"); ok { - v, errRet := strconv.ParseInt(tmpStr, 10, 64) - if errRet != nil || v < 0 { - err = fmt.Errorf("Parse conf error: invalid vhost_http_timeout") - return - } - cfg.VhostHTTPTimeout = v - } - - if tmpStr, ok = conf.Get("common", "dashboard_addr"); ok { - cfg.DashboardAddr = tmpStr - } else { - cfg.DashboardAddr = cfg.BindAddr - } - - if tmpStr, ok = conf.Get("common", "dashboard_port"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid dashboard_port") - return - } - cfg.DashboardPort = int(v) - } else { - cfg.DashboardPort = 0 - } - - if tmpStr, ok = conf.Get("common", "dashboard_user"); ok { - cfg.DashboardUser = tmpStr - } - - if tmpStr, ok = conf.Get("common", "dashboard_pwd"); ok { - cfg.DashboardPwd = tmpStr - } - - if tmpStr, ok = conf.Get("common", "enable_prometheus"); ok && tmpStr == "true" { - cfg.EnablePrometheus = true - } - - if tmpStr, ok = conf.Get("common", "assets_dir"); ok { - cfg.AssetsDir = tmpStr - } - - if tmpStr, ok = conf.Get("common", "log_file"); ok { - cfg.LogFile = tmpStr - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } - } - - if tmpStr, ok = conf.Get("common", "log_level"); ok { - cfg.LogLevel = tmpStr - } - - if tmpStr, ok = conf.Get("common", "log_max_days"); ok { - v, err = strconv.ParseInt(tmpStr, 10, 64) - if err == nil { - cfg.LogMaxDays = v - } - } - - if tmpStr, ok = conf.Get("common", "disable_log_color"); ok && tmpStr == "true" { - cfg.DisableLogColor = true - } - - if tmpStr, ok = conf.Get("common", "detailed_errors_to_client"); ok && tmpStr == "false" { - cfg.DetailedErrorsToClient = false - } else { - cfg.DetailedErrorsToClient = true - } - - if allowPortsStr, ok := conf.Get("common", "allow_ports"); ok { - // e.g. 1000-2000,2001,2002,3000-4000 - ports, errRet := util.ParseRangeNumbers(allowPortsStr) - if errRet != nil { - err = fmt.Errorf("Parse conf error: allow_ports: %v", errRet) - return - } - - for _, port := range ports { - cfg.AllowPorts[int(port)] = struct{}{} - } - } - - if tmpStr, ok = conf.Get("common", "max_pool_count"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid max_pool_count") - return - } - - if v < 0 { - err = fmt.Errorf("Parse conf error: invalid max_pool_count") - return - } - cfg.MaxPoolCount = v - } - - if tmpStr, ok = conf.Get("common", "max_ports_per_client"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid max_ports_per_client") - return - } - - if v < 0 { - err = fmt.Errorf("Parse conf error: invalid max_ports_per_client") - return - } - cfg.MaxPortsPerClient = v - } - - if tmpStr, ok = conf.Get("common", "subdomain_host"); ok { - cfg.SubDomainHost = strings.ToLower(strings.TrimSpace(tmpStr)) - } - - if tmpStr, ok = conf.Get("common", "tcp_mux"); ok && tmpStr == "false" { - cfg.TCPMux = false - } else { - cfg.TCPMux = true - } - - if tmpStr, ok = conf.Get("common", "custom_404_page"); ok { - cfg.Custom404Page = tmpStr - } - - if tmpStr, ok = conf.Get("common", "heartbeat_timeout"); ok { - v, errRet := strconv.ParseInt(tmpStr, 10, 64) - if errRet != nil { - err = fmt.Errorf("Parse conf error: heartbeat_timeout is incorrect") - return - } - cfg.HeartbeatTimeout = v - } - - if tmpStr, ok = conf.Get("common", "tls_only"); ok && tmpStr == "true" { - cfg.TLSOnly = true - } else { - cfg.TLSOnly = false - } - - if tmpStr, ok = conf.Get("common", "udp_packet_size"); ok { - if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { - err = fmt.Errorf("Parse conf error: invalid udp_packet_size") - return - } - cfg.UDPPacketSize = v - } - - if tmpStr, ok := conf.Get("common", "tls_cert_file"); ok { - cfg.TLSCertFile = tmpStr - } - - if tmpStr, ok := conf.Get("common", "tls_key_file"); ok { - cfg.TLSKeyFile = tmpStr - } - - if tmpStr, ok := conf.Get("common", "tls_trusted_ca_file"); ok { - cfg.TLSTrustedCaFile = tmpStr - cfg.TLSOnly = true - } - - return -} - -func UnmarshalPluginsFromIni(sections ini.File, cfg *ServerCommonConf) { - for name, section := range sections { - if strings.HasPrefix(name, "plugin.") { - name = strings.TrimSpace(strings.TrimPrefix(name, "plugin.")) - var tls_verify, err = strconv.ParseBool(section["tls_verify"]) - if err != nil { - tls_verify = true - } - options := plugin.HTTPPluginOptions{ - Name: name, - Addr: section["addr"], - Path: section["path"], - Ops: strings.Split(section["ops"], ","), - TLSVerify: tls_verify, - } - for i := range options.Ops { - options.Ops[i] = strings.TrimSpace(options.Ops[i]) - } - cfg.HTTPPlugins[name] = options - } - } -} - -func (cfg *ServerCommonConf) Check() error { - return nil -} diff --git a/pkg/config/server_test.go b/pkg/config/server_test.go new file mode 100644 index 00000000..f60c2b83 --- /dev/null +++ b/pkg/config/server_test.go @@ -0,0 +1,207 @@ +// Copyright 2020 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 config + +import ( + "testing" + + "github.com/fatedier/frp/pkg/auth" + "github.com/fatedier/frp/pkg/plugin/server" + + "github.com/stretchr/testify/assert" +) + +func Test_LoadServerCommonConf(t *testing.T) { + assert := assert.New(t) + + testcases := []struct { + source []byte + expected ServerCommonConf + }{ + { + source: []byte(` + # [common] is integral section + [common] + bind_addr = 0.0.0.9 + bind_port = 7009 + bind_udp_port = 7008 + kcp_bind_port = 7007 + proxy_bind_addr = 127.0.0.9 + vhost_http_port = 89 + vhost_https_port = 449 + vhost_http_timeout = 69 + tcpmux_httpconnect_port = 1339 + dashboard_addr = 0.0.0.9 + dashboard_port = 7509 + dashboard_user = admin9 + dashboard_pwd = admin9 + enable_prometheus + assets_dir = ./static9 + log_file = ./frps.log9 + log_way = file + log_level = info9 + log_max_days = 39 + disable_log_color = false + detailed_errors_to_client + authentication_method = token + authenticate_heartbeats = false + authenticate_new_work_conns = false + token = 123456789 + oidc_issuer = test9 + oidc_audience = test9 + oidc_skip_expiry_check + oidc_skip_issuer_check + heartbeat_timeout = 99 + user_conn_timeout = 9 + allow_ports = 10-12,99 + max_pool_count = 59 + max_ports_per_client = 9 + tls_only = false + tls_cert_file = server.crt + tls_key_file = server.key + tls_trusted_ca_file = ca.crt + subdomain_host = frps.com + tcp_mux + udp_packet_size = 1509 + [plugin.user-manager] + addr = 127.0.0.1:9009 + path = /handler + ops = Login + [plugin.port-manager] + addr = 127.0.0.1:9009 + path = /handler + ops = NewProxy + tls_verify + `), + expected: ServerCommonConf{ + ServerConfig: auth.ServerConfig{ + BaseConfig: auth.BaseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + }, + TokenConfig: auth.TokenConfig{ + Token: "123456789", + }, + OidcServerConfig: auth.OidcServerConfig{ + OidcIssuer: "test9", + OidcAudience: "test9", + OidcSkipExpiryCheck: true, + OidcSkipIssuerCheck: true, + }, + }, + BindAddr: "0.0.0.9", + BindPort: 7009, + BindUDPPort: 7008, + KCPBindPort: 7007, + ProxyBindAddr: "127.0.0.9", + VhostHTTPPort: 89, + VhostHTTPSPort: 449, + VhostHTTPTimeout: 69, + TCPMuxHTTPConnectPort: 1339, + DashboardAddr: "0.0.0.9", + DashboardPort: 7509, + DashboardUser: "admin9", + DashboardPwd: "admin9", + EnablePrometheus: true, + AssetsDir: "./static9", + LogFile: "./frps.log9", + LogWay: "file", + LogLevel: "info9", + LogMaxDays: 39, + DisableLogColor: false, + DetailedErrorsToClient: true, + HeartbeatTimeout: 99, + UserConnTimeout: 9, + AllowPorts: map[int]struct{}{ + 10: struct{}{}, + 11: struct{}{}, + 12: struct{}{}, + 99: struct{}{}, + }, + MaxPoolCount: 59, + MaxPortsPerClient: 9, + TLSOnly: false, + TLSCertFile: "server.crt", + TLSKeyFile: "server.key", + TLSTrustedCaFile: "ca.crt", + SubDomainHost: "frps.com", + TCPMux: true, + UDPPacketSize: 1509, + + HTTPPlugins: map[string]plugin.HTTPPluginOptions{ + "user-manager": { + Name: "user-manager", + Addr: "127.0.0.1:9009", + Path: "/handler", + Ops: []string{"Login"}, + }, + "port-manager": { + Name: "port-manager", + Addr: "127.0.0.1:9009", + Path: "/handler", + Ops: []string{"NewProxy"}, + TLSVerify: true, + }, + }, + }, + }, + { + source: []byte(` + # [common] is integral section + [common] + bind_addr = 0.0.0.9 + bind_port = 7009 + bind_udp_port = 7008 + `), + expected: ServerCommonConf{ + ServerConfig: auth.ServerConfig{ + BaseConfig: auth.BaseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + }, + }, + BindAddr: "0.0.0.9", + BindPort: 7009, + BindUDPPort: 7008, + ProxyBindAddr: "0.0.0.0", + VhostHTTPTimeout: 60, + DashboardAddr: "0.0.0.0", + DashboardUser: "admin", + DashboardPwd: "admin", + EnablePrometheus: false, + LogFile: "console", + LogWay: "console", + LogLevel: "info", + LogMaxDays: 3, + DetailedErrorsToClient: true, + TCPMux: true, + AllowPorts: make(map[int]struct{}), + MaxPoolCount: 5, + HeartbeatTimeout: 90, + UserConnTimeout: 10, + HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), + UDPPacketSize: 1500, + }, + }, + } + + for _, c := range testcases { + actual, err := UnmarshalServerConfFromIni(c.source) + assert.NoError(err) + assert.Equal(c.expected, actual) + } +} diff --git a/pkg/config/types.go b/pkg/config/types.go index 87c240d5..062cc746 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -41,6 +41,15 @@ func NewBandwidthQuantity(s string) (BandwidthQuantity, error) { return q, nil } +func MustBandwidthQuantity(s string) BandwidthQuantity { + q := BandwidthQuantity{} + err := q.UnmarshalString(s) + if err != nil { + panic(err) + } + return q +} + func (q *BandwidthQuantity) Equal(u *BandwidthQuantity) bool { if q == nil && u == nil { return true diff --git a/pkg/config/utils.go b/pkg/config/utils.go new file mode 100644 index 00000000..aef674d4 --- /dev/null +++ b/pkg/config/utils.go @@ -0,0 +1,51 @@ +// Copyright 2020 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 config + +import ( + "strings" +) + +func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string { + m := make(map[string]string) + + for key, value := range set { + if strings.HasPrefix(key, prefix) { + m[strings.TrimPrefix(key, prefix)] = value + } + } + + if len(m) == 0 { + return nil + } + + return m +} + +func GetMapByPrefix(set map[string]string, prefix string) map[string]string { + m := make(map[string]string) + + for key, value := range set { + if strings.HasPrefix(key, prefix) { + m[key] = value + } + } + + if len(m) == 0 { + return nil + } + + return m +} diff --git a/pkg/config/value.go b/pkg/config/value.go index 34570248..a3114e6b 100644 --- a/pkg/config/value.go +++ b/pkg/config/value.go @@ -1,3 +1,17 @@ +// Copyright 2020 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 config import ( @@ -34,8 +48,8 @@ func GetValues() *Values { } } -func RenderContent(in string) (out string, err error) { - tmpl, errRet := template.New("frp").Parse(in) +func RenderContent(in []byte) (out []byte, err error) { + tmpl, errRet := template.New("frp").Parse(string(in)) if errRet != nil { err = errRet return @@ -47,18 +61,17 @@ func RenderContent(in string) (out string, err error) { if err != nil { return } - out = buffer.String() + out = buffer.Bytes() return } -func GetRenderedConfFromFile(path string) (out string, err error) { +func GetRenderedConfFromFile(path string) (out []byte, err error) { var b []byte b, err = ioutil.ReadFile(path) if err != nil { return } - content := string(b) - out, err = RenderContent(content) + out, err = RenderContent(b) return } diff --git a/pkg/config/visitor.go b/pkg/config/visitor.go index 39958ea5..11959eca 100644 --- a/pkg/config/visitor.go +++ b/pkg/config/visitor.go @@ -17,72 +17,89 @@ package config import ( "fmt" "reflect" - "strconv" "github.com/fatedier/frp/pkg/consts" - ini "github.com/vaughan0/go-ini" + "gopkg.in/ini.v1" ) +// Visitor var ( - visitorConfTypeMap map[string]reflect.Type + visitorConfTypeMap = map[string]reflect.Type{ + consts.STCPProxy: reflect.TypeOf(STCPVisitorConf{}), + consts.XTCPProxy: reflect.TypeOf(XTCPVisitorConf{}), + consts.SUDPProxy: reflect.TypeOf(SUDPVisitorConf{}), + } ) -func init() { - visitorConfTypeMap = make(map[string]reflect.Type) - visitorConfTypeMap[consts.STCPProxy] = reflect.TypeOf(STCPVisitorConf{}) - visitorConfTypeMap[consts.XTCPProxy] = reflect.TypeOf(XTCPVisitorConf{}) - visitorConfTypeMap[consts.SUDPProxy] = reflect.TypeOf(SUDPVisitorConf{}) -} - type VisitorConf interface { GetBaseInfo() *BaseVisitorConf Compare(cmp VisitorConf) bool - UnmarshalFromIni(prefix string, name string, section ini.Section) error + UnmarshalFromIni(prefix string, name string, section *ini.Section) error Check() error } -func NewVisitorConfByType(cfgType string) VisitorConf { - v, ok := visitorConfTypeMap[cfgType] +type BaseVisitorConf struct { + ProxyName string `ini:"name" json:"name"` + ProxyType string `ini:"type" json:"type"` + UseEncryption bool `ini:"use_encryption" json:"use_encryption"` + UseCompression bool `ini:"use_compression" json:"use_compression"` + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` + ServerName string `ini:"server_name" json:"server_name"` + BindAddr string `ini:"bind_addr" json:"bind_addr"` + BindPort int `ini:"bind_port" json:"bind_port"` +} + +type SUDPVisitorConf struct { + BaseVisitorConf `ini:",extends" json:"inline"` +} + +type STCPVisitorConf struct { + BaseVisitorConf `ini:",extends" json:"inline"` +} + +type XTCPVisitorConf struct { + BaseVisitorConf `ini:",extends" json:"inline"` +} + +// DefaultVisitorConf creates a empty VisitorConf object by visitorType. +// If visitorType doesn't exist, return nil. +func DefaultVisitorConf(visitorType string) VisitorConf { + v, ok := visitorConfTypeMap[visitorType] if !ok { return nil } - cfg := reflect.New(v).Interface().(VisitorConf) - return cfg + + return reflect.New(v).Interface().(VisitorConf) } -func NewVisitorConfFromIni(prefix string, name string, section ini.Section) (cfg VisitorConf, err error) { - cfgType := section["type"] - if cfgType == "" { - err = fmt.Errorf("visitor [%s] type shouldn't be empty", name) - return +// Visitor loaded from ini +func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) { + // section.Key: if key not exists, section will set it with default value. + visitorType := section.Key("type").String() + + if visitorType == "" { + return nil, fmt.Errorf("visitor [%s] type shouldn't be empty", name) } - cfg = NewVisitorConfByType(cfgType) - if cfg == nil { - err = fmt.Errorf("visitor [%s] type [%s] error", name, cfgType) - return + + conf := DefaultVisitorConf(visitorType) + if conf == nil { + return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) } - if err = cfg.UnmarshalFromIni(prefix, name, section); err != nil { - return + + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, fmt.Errorf("visitor [%s] type [%s] error", name, visitorType) } - if err = cfg.Check(); err != nil { - return + + if err := conf.Check(); err != nil { + return nil, err } - return -} - -type BaseVisitorConf struct { - ProxyName string `json:"proxy_name"` - ProxyType string `json:"proxy_type"` - UseEncryption bool `json:"use_encryption"` - UseCompression bool `json:"use_compression"` - Role string `json:"role"` - Sk string `json:"sk"` - ServerName string `json:"server_name"` - BindAddr string `json:"bind_addr"` - BindPort int `json:"bind_port"` + + return conf, nil } +// Base func (cfg *BaseVisitorConf) GetBaseInfo() *BaseVisitorConf { return cfg } @@ -118,45 +135,40 @@ func (cfg *BaseVisitorConf) check() (err error) { return } -func (cfg *BaseVisitorConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - var ( - tmpStr string - ok bool - ) +func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error { + + // Custom decoration after basic unmarshal: + // proxy name cfg.ProxyName = prefix + name - cfg.ProxyType = section["type"] - if tmpStr, ok = section["use_encryption"]; ok && tmpStr == "true" { - cfg.UseEncryption = true - } - if tmpStr, ok = section["use_compression"]; ok && tmpStr == "true" { - cfg.UseCompression = true - } + // server_name + cfg.ServerName = prefix + cfg.ServerName - cfg.Role = section["role"] - if cfg.Role != "visitor" { - return fmt.Errorf("Parse conf error: proxy [%s] incorrect role [%s]", name, cfg.Role) - } - cfg.Sk = section["sk"] - cfg.ServerName = prefix + section["server_name"] - if cfg.BindAddr = section["bind_addr"]; cfg.BindAddr == "" { + // bind_addr + if cfg.BindAddr == "" { cfg.BindAddr = "127.0.0.1" } - if tmpStr, ok = section["bind_port"]; ok { - if cfg.BindPort, err = strconv.Atoi(tmpStr); err != nil { - return fmt.Errorf("Parse conf error: proxy [%s] bind_port incorrect", name) - } - } else { - return fmt.Errorf("Parse conf error: proxy [%s] bind_port not found", name) - } return nil } -type SUDPVisitorConf struct { - BaseVisitorConf +func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, section *ini.Section) error { + err := section.MapTo(cfg) + if err != nil { + return err + } + + err = cfg.GetBaseInfo().unmarshalFromIni(prefix, name, section) + if err != nil { + return err + } + + return nil } +// SUDP +var _ VisitorConf = &SUDPVisitorConf{} + func (cfg *SUDPVisitorConf) Compare(cmp VisitorConf) bool { cmpConf, ok := cmp.(*SUDPVisitorConf) if !ok { @@ -166,13 +178,20 @@ func (cfg *SUDPVisitorConf) Compare(cmp VisitorConf) bool { if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { return false } + + // Add custom login equal, if exists + return true } -func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseVisitorConf.UnmarshalFromIni(prefix, name, section); err != nil { +func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { return } + + // Add custom logic unmarshal, if exists + return } @@ -180,12 +199,14 @@ func (cfg *SUDPVisitorConf) Check() (err error) { if err = cfg.BaseVisitorConf.check(); err != nil { return } + + // Add custom logic validate, if exists + return } -type STCPVisitorConf struct { - BaseVisitorConf -} +// STCP +var _ VisitorConf = &STCPVisitorConf{} func (cfg *STCPVisitorConf) Compare(cmp VisitorConf) bool { cmpConf, ok := cmp.(*STCPVisitorConf) @@ -196,13 +217,20 @@ func (cfg *STCPVisitorConf) Compare(cmp VisitorConf) bool { if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { return false } + + // Add custom login equal, if exists + return true } -func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseVisitorConf.UnmarshalFromIni(prefix, name, section); err != nil { +func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { return } + + // Add custom logic unmarshal, if exists + return } @@ -210,12 +238,14 @@ func (cfg *STCPVisitorConf) Check() (err error) { if err = cfg.BaseVisitorConf.check(); err != nil { return } + + // Add custom logic validate, if exists + return } -type XTCPVisitorConf struct { - BaseVisitorConf -} +// XTCP +var _ VisitorConf = &XTCPVisitorConf{} func (cfg *XTCPVisitorConf) Compare(cmp VisitorConf) bool { cmpConf, ok := cmp.(*XTCPVisitorConf) @@ -226,13 +256,20 @@ func (cfg *XTCPVisitorConf) Compare(cmp VisitorConf) bool { if !cfg.BaseVisitorConf.compare(&cmpConf.BaseVisitorConf) { return false } + + // Add custom login equal, if exists + return true } -func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { - if err = cfg.BaseVisitorConf.UnmarshalFromIni(prefix, name, section); err != nil { +func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { + err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { return } + + // Add custom logic unmarshal, if exists + return } @@ -240,5 +277,8 @@ func (cfg *XTCPVisitorConf) Check() (err error) { if err = cfg.BaseVisitorConf.check(); err != nil { return } + + // Add custom logic validate, if exists + return } diff --git a/pkg/config/visitor_test.go b/pkg/config/visitor_test.go new file mode 100644 index 00000000..ce200ed0 --- /dev/null +++ b/pkg/config/visitor_test.go @@ -0,0 +1,108 @@ +// Copyright 2020 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 config + +import ( + "testing" + + "github.com/fatedier/frp/pkg/consts" + + "github.com/stretchr/testify/assert" + "gopkg.in/ini.v1" +) + +const testVisitorPrefix = "test." + +func Test_Visitor_Interface(t *testing.T) { + for name := range visitorConfTypeMap { + DefaultVisitorConf(name) + } +} + +func Test_Visitor_UnmarshalFromIni(t *testing.T) { + assert := assert.New(t) + + testcases := []struct { + sname string + source []byte + expected VisitorConf + }{ + { + sname: "secret_tcp_visitor", + source: []byte(` + [secret_tcp_visitor] + role = visitor + type = stcp + server_name = secret_tcp + sk = abcdefg + bind_addr = 127.0.0.1 + bind_port = 9000 + use_encryption = false + use_compression = false + `), + expected: &STCPVisitorConf{ + BaseVisitorConf: BaseVisitorConf{ + ProxyName: testVisitorPrefix + "secret_tcp_visitor", + ProxyType: consts.STCPProxy, + Role: "visitor", + Sk: "abcdefg", + ServerName: testVisitorPrefix + "secret_tcp", + BindAddr: "127.0.0.1", + BindPort: 9000, + }, + }, + }, + { + sname: "p2p_tcp_visitor", + source: []byte(` + [p2p_tcp_visitor] + role = visitor + type = xtcp + server_name = p2p_tcp + sk = abcdefg + bind_addr = 127.0.0.1 + bind_port = 9001 + use_encryption = false + use_compression = false + `), + expected: &XTCPVisitorConf{ + BaseVisitorConf: BaseVisitorConf{ + ProxyName: testVisitorPrefix + "p2p_tcp_visitor", + ProxyType: consts.XTCPProxy, + Role: "visitor", + Sk: "abcdefg", + ServerName: testProxyPrefix + "p2p_tcp", + BindAddr: "127.0.0.1", + BindPort: 9001, + }, + }, + }, + } + + for _, c := range testcases { + f, err := ini.LoadSources(testLoadOptions, c.source) + assert.NoError(err) + + visitorType := f.Section(c.sname).Key("type").String() + assert.NotEmpty(visitorType) + + actual := DefaultVisitorConf(visitorType) + assert.NotNil(actual) + + err = actual.UnmarshalFromIni(testVisitorPrefix, c.sname, f.Section(c.sname)) + assert.NoError(err) + assert.Equal(c.expected, actual) + } +} diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go new file mode 100644 index 00000000..159ed398 --- /dev/null +++ b/pkg/plugin/client/https2https.go @@ -0,0 +1,138 @@ +// Copyright 2019 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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 plugin + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + + frpNet "github.com/fatedier/frp/pkg/util/net" +) + +const PluginHTTPS2HTTPS = "https2https" + +func init() { + Register(PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin) +} + +type HTTPS2HTTPSPlugin struct { + crtPath string + keyPath string + hostHeaderRewrite string + localAddr string + headers map[string]string + + l *Listener + s *http.Server +} + +func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { + crtPath := params["plugin_crt_path"] + keyPath := params["plugin_key_path"] + localAddr := params["plugin_local_addr"] + hostHeaderRewrite := params["plugin_host_header_rewrite"] + headers := make(map[string]string) + for k, v := range params { + if !strings.HasPrefix(k, "plugin_header_") { + continue + } + if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { + headers[k] = v + } + } + + if crtPath == "" { + return nil, fmt.Errorf("plugin_crt_path is required") + } + if keyPath == "" { + return nil, fmt.Errorf("plugin_key_path is required") + } + if localAddr == "" { + return nil, fmt.Errorf("plugin_local_addr is required") + } + + listener := NewProxyListener() + + p := &HTTPS2HTTPSPlugin{ + crtPath: crtPath, + keyPath: keyPath, + localAddr: localAddr, + hostHeaderRewrite: hostHeaderRewrite, + headers: headers, + l: listener, + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + rp := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = "https" + req.URL.Host = p.localAddr + if p.hostHeaderRewrite != "" { + req.Host = p.hostHeaderRewrite + } + for k, v := range p.headers { + req.Header.Set(k, v) + } + }, + Transport: tr, + } + + p.s = &http.Server{ + Handler: rp, + } + + tlsConfig, err := p.genTLSConfig() + if err != nil { + return nil, fmt.Errorf("gen TLS config error: %v", err) + } + ln := tls.NewListener(listener, tlsConfig) + + go p.s.Serve(ln) + return p, nil +} + +func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + if err != nil { + return nil, err + } + + config := &tls.Config{Certificates: []tls.Certificate{cert}} + return config, nil +} + +func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) { + wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) + p.l.PutConn(wrapConn) +} + +func (p *HTTPS2HTTPSPlugin) Name() string { + return PluginHTTPS2HTTP +} + +func (p *HTTPS2HTTPSPlugin) Close() error { + if err := p.s.Close(); err != nil { + return err + } + return nil +} diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 696f8617..f4d9c5ce 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -28,11 +28,11 @@ import ( ) type HTTPPluginOptions struct { - Name string - Addr string - Path string - Ops []string - TLSVerify bool + Name string `ini:"name"` + Addr string `ini:"addr"` + Path string `ini:"path"` + Ops []string `ini:"ops"` + TLSVerify bool `ini:"tls_verify"` } type httpPlugin struct { diff --git a/pkg/transport/tls.go b/pkg/transport/tls.go index a266cb8a..e95b4c72 100644 --- a/pkg/transport/tls.go +++ b/pkg/transport/tls.go @@ -43,7 +43,7 @@ func newRandomTLSKeyPair() *tls.Certificate { return &tlsCert } -// Only supprt one ca file to add +// Only support one ca file to add func newCertPool(caPath string) (*x509.CertPool, error) { pool := x509.NewCertPool() diff --git a/pkg/util/net/kcp.go b/pkg/util/net/kcp.go index 2788b5f8..e9e0b126 100644 --- a/pkg/util/net/kcp.go +++ b/pkg/util/net/kcp.go @@ -27,8 +27,8 @@ type KCPListener struct { closeFlag bool } -func ListenKcp(bindAddr string, bindPort int) (l *KCPListener, err error) { - listener, err := kcp.ListenWithOptions(fmt.Sprintf("%s:%d", bindAddr, bindPort), nil, 10, 3) +func ListenKcp(address string) (l *KCPListener, err error) { + listener, err := kcp.ListenWithOptions(address, nil, 10, 3) if err != nil { return l, err } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index d11e2743..6cbe16a9 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.35.1" +var version string = "0.36.0" func Full() string { return version diff --git a/server/dashboard.go b/server/dashboard.go index 7af9df27..edac675e 100644 --- a/server/dashboard.go +++ b/server/dashboard.go @@ -15,7 +15,6 @@ package server import ( - "fmt" "net" "net/http" "time" @@ -32,7 +31,7 @@ var ( httpServerWriteTimeout = 10 * time.Second ) -func (svr *Service) RunDashboardServer(addr string, port int) (err error) { +func (svr *Service) RunDashboardServer(address string) (err error) { // url router router := mux.NewRouter() @@ -58,14 +57,13 @@ func (svr *Service) RunDashboardServer(addr string, port int) (err error) { http.Redirect(w, r, "/static/", http.StatusMovedPermanently) }) - address := fmt.Sprintf("%s:%d", addr, port) server := &http.Server{ Addr: address, Handler: router, ReadTimeout: httpServerReadTimeout, WriteTimeout: httpServerWriteTimeout, } - if address == "" { + if address == "" || address == ":" { address = ":http" } ln, err := net.Listen("tcp", address) diff --git a/server/service.go b/server/service.go index 0bbe9552..fe54cf78 100644 --- a/server/service.go +++ b/server/service.go @@ -23,6 +23,7 @@ import ( "net" "net/http" "sort" + "strconv" "time" "github.com/fatedier/frp/assets" @@ -176,7 +177,8 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { } // Listen for accepting connections from client. - ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.BindPort)) + address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort)) + ln, err := net.Listen("tcp", address) if err != nil { err = fmt.Errorf("Create server listener error, %v", err) return @@ -187,13 +189,14 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { ln = svr.muxer.DefaultListener() svr.listener = ln - log.Info("frps tcp listen on %s:%d", cfg.BindAddr, cfg.BindPort) + log.Info("frps tcp listen on %s", address) // Listen for accepting connections from client using kcp protocol. if cfg.KCPBindPort > 0 { - svr.kcpListener, err = frpNet.ListenKcp(cfg.BindAddr, cfg.KCPBindPort) + address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort)) + svr.kcpListener, err = frpNet.ListenKcp(address) if err != nil { - err = fmt.Errorf("Listen on kcp address udp [%s:%d] error: %v", cfg.BindAddr, cfg.KCPBindPort, err) + err = fmt.Errorf("Listen on kcp address udp %s error: %v", address, err) return } log.Info("frps kcp listen on udp %s:%d", cfg.BindAddr, cfg.KCPBindPort) @@ -213,7 +216,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { }, svr.httpVhostRouter) svr.rc.HTTPReverseProxy = rp - address := fmt.Sprintf("%s:%d", cfg.ProxyBindAddr, cfg.VhostHTTPPort) + address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort)) server := &http.Server{ Addr: address, Handler: rp, @@ -238,11 +241,13 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { if httpsMuxOn { l = svr.muxer.ListenHttps(1) } else { - l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.ProxyBindAddr, cfg.VhostHTTPSPort)) + address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort)) + l, err = net.Listen("tcp", address) if err != nil { err = fmt.Errorf("Create server listener error, %v", err) return } + log.Info("https service listen on %s", address) } svr.rc.VhostHTTPSMuxer, err = vhost.NewHTTPSMuxer(l, vhostReadWriteTimeout) @@ -250,7 +255,6 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { err = fmt.Errorf("Create vhost httpsMuxer error, %v", err) return } - log.Info("https service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostHTTPSPort) } // frp tls listener @@ -261,14 +265,14 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { // Create nat hole controller. if cfg.BindUDPPort > 0 { var nc *nathole.Controller - addr := fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.BindUDPPort) - nc, err = nathole.NewController(addr) + address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.BindPort)) + nc, err = nathole.NewController(address) if err != nil { err = fmt.Errorf("Create nat hole controller error, %v", err) return } svr.rc.NatHoleController = nc - log.Info("nat hole udp service listen on %s:%d", cfg.BindAddr, cfg.BindUDPPort) + log.Info("nat hole udp service listen on %s", address) } var statsEnable bool @@ -281,7 +285,8 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { return } - err = svr.RunDashboardServer(cfg.DashboardAddr, cfg.DashboardPort) + address := net.JoinHostPort(cfg.DashboardAddr, strconv.Itoa(cfg.DashboardPort)) + err = svr.RunDashboardServer(address) if err != nil { err = fmt.Errorf("Create dashboard web server error, %v", err) return diff --git a/tests/ci/cmd_test.go b/tests/ci/cmd_test.go index 8a0b4775..4a98c446 100644 --- a/tests/ci/cmd_test.go +++ b/tests/ci/cmd_test.go @@ -19,7 +19,7 @@ func TestCmdTCP(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"tcp", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-l", "10701", "-r", "20801", "-n", "tcp_test"}) @@ -43,7 +43,7 @@ func TestCmdUDP(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"udp", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-l", "10702", "-r", "20802", "-n", "udp_test"}) @@ -67,7 +67,7 @@ func TestCmdHTTP(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"http", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-n", "udp_test", "-l", "10704", "--custom_domain", "127.0.0.1"}) diff --git a/tests/ci/health/health_test.go b/tests/ci/health/health_test.go index 0e75ad22..102511a6 100644 --- a/tests/ci/health/health_test.go +++ b/tests/ci/health/health_test.go @@ -175,7 +175,7 @@ func TestHealthCheck(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(500 * time.Millisecond) frpcProcess := util.NewProcess(consts.FRPC_SUB_BIN_PATH, []string{"-c", frpcCfgPath}) err = frpcProcess.Start() diff --git a/tests/ci/normal_test.go b/tests/ci/normal_test.go index e9e34945..f1dba7a1 100644 --- a/tests/ci/normal_test.go +++ b/tests/ci/normal_test.go @@ -42,7 +42,7 @@ func TestMain(m *testing.M) { panic(err) } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) p2 := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", "./auto_test_frpc.ini"}) if err = p2.Start(); err != nil { panic(err) diff --git a/tests/ci/reconnect_test.go b/tests/ci/reconnect_test.go index 86ab317b..6455347f 100644 --- a/tests/ci/reconnect_test.go +++ b/tests/ci/reconnect_test.go @@ -56,7 +56,7 @@ func TestReconnect(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) err = frpcProcess.Start() diff --git a/tests/ci/reload_test.go b/tests/ci/reload_test.go index 73a7a091..ac160fa5 100644 --- a/tests/ci/reload_test.go +++ b/tests/ci/reload_test.go @@ -94,7 +94,7 @@ func TestReload(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) err = frpcProcess.Start() diff --git a/tests/ci/template_test.go b/tests/ci/template_test.go index f1361212..0c843cd8 100644 --- a/tests/ci/template_test.go +++ b/tests/ci/template_test.go @@ -55,7 +55,7 @@ func TestConfTemplate(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(200 * time.Millisecond) + time.Sleep(500 * time.Millisecond) frpcProcess := util.NewProcess("env", []string{"FRP_TOKEN=123456", "TCP_REMOTE_PORT=20801", consts.FRPC_BIN_PATH, "-c", frpcCfgPath}) err = frpcProcess.Start()