forked from Mxmilu666/frp
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e66f45d8be
|
|||
|
cc0b8d0f94
|
|||
|
27237542c8
|
|||
|
406ea5ebee
|
|||
|
81a92d19d5
|
|||
|
83847f32ed
|
|||
|
1f2c26761d
|
|||
|
c836e276a7
|
|||
| d16f07d3e8 | |||
|
6bd5eb92c3
|
|||
|
09602f5d74
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
93
.github/workflows/release.yaml
vendored
93
.github/workflows/release.yaml
vendored
@@ -91,15 +91,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Save checksums for changelog
|
||||
id: checksums
|
||||
run: |
|
||||
{
|
||||
echo 'content<<EOF'
|
||||
cat release_files/sha256sum.txt
|
||||
echo EOF
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Debug - Check tags and commits
|
||||
run: |
|
||||
echo "Current tag: ${{ steps.tag.outputs.tag }}"
|
||||
@@ -117,89 +108,19 @@ jobs:
|
||||
|
||||
- name: Build Changelog
|
||||
id: changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
uses: requarks/changelog-action@v1
|
||||
with:
|
||||
configurationJson: |
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"title": "## 🚀 Features",
|
||||
"labels": ["feature", "feat", "enhancement"]
|
||||
},
|
||||
{
|
||||
"title": "## 🐛 Bug Fixes",
|
||||
"labels": ["fix", "bug", "bugfix"]
|
||||
},
|
||||
{
|
||||
"title": "## 📝 Documentation",
|
||||
"labels": ["docs", "documentation"]
|
||||
},
|
||||
{
|
||||
"title": "## 🔧 Maintenance",
|
||||
"labels": ["chore", "refactor", "perf"]
|
||||
},
|
||||
{
|
||||
"title": "## 📦 Dependencies",
|
||||
"labels": ["dependencies", "deps"]
|
||||
},
|
||||
{
|
||||
"title": "## 🔀 Other Changes",
|
||||
"labels": []
|
||||
}
|
||||
],
|
||||
"template": "#{{CHANGELOG}}\n\n## 📥 Download\n\n### Checksums (SHA256)\n\n```\n${{ steps.checksums.outputs.content }}\n```\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
|
||||
"pr_template": "- #{{TITLE}} by @#{{AUTHOR}} in ##{{NUMBER}}",
|
||||
"commit_template": "- #{{TITLE}} by @#{{AUTHOR}}",
|
||||
"empty_template": "- No changes in this release",
|
||||
"mode": "COMMIT",
|
||||
"sort": {
|
||||
"order": "ASC",
|
||||
"on_property": "mergedAt"
|
||||
},
|
||||
"max_tags_to_fetch": 200,
|
||||
"max_pull_requests": 200,
|
||||
"max_back_track_time_days": 365,
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "^(feat|feature)(\\(.+\\))?:",
|
||||
"target": "feature",
|
||||
"on_property": "title"
|
||||
},
|
||||
{
|
||||
"pattern": "^fix(\\(.+\\))?:",
|
||||
"target": "fix",
|
||||
"on_property": "title"
|
||||
},
|
||||
{
|
||||
"pattern": "^docs(\\(.+\\))?:",
|
||||
"target": "docs",
|
||||
"on_property": "title"
|
||||
},
|
||||
{
|
||||
"pattern": "^(chore|refactor|perf)(\\(.+\\))?:",
|
||||
"target": "chore",
|
||||
"on_property": "title"
|
||||
},
|
||||
{
|
||||
"pattern": "^(deps|dependencies)(\\(.+\\))?:",
|
||||
"target": "dependencies",
|
||||
"on_property": "title"
|
||||
}
|
||||
],
|
||||
"ignore_labels": ["ignore", "no-changelog"],
|
||||
"ignore_commits": ["skip-ci", "skip ci", "[skip ci]"]
|
||||
}
|
||||
outputFile: CHANGELOG.md
|
||||
toTag: ${{ steps.tag.outputs.tag }}
|
||||
commitMode: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
writeToFile: false
|
||||
includeInvalidCommits: true
|
||||
useGitmojis: false
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.tag }}
|
||||
name: Release ${{ steps.tag.outputs.tag }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
body: ${{ steps.changelog.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,3 +42,6 @@ client.key
|
||||
|
||||
# AI
|
||||
CLAUDE.md
|
||||
|
||||
# TLS
|
||||
.autotls-cache
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -69,6 +71,7 @@ func NewProxy(
|
||||
|
||||
baseProxy := BaseProxy{
|
||||
baseCfg: pxyConf.GetBaseConfig(),
|
||||
configurer: pxyConf,
|
||||
clientCfg: clientCfg,
|
||||
encryptionKey: encryptionKey,
|
||||
limiter: limiter,
|
||||
@@ -87,6 +90,7 @@ func NewProxy(
|
||||
|
||||
type BaseProxy struct {
|
||||
baseCfg *v1.ProxyBaseConfig
|
||||
configurer v1.ProxyConfigurer
|
||||
clientCfg *v1.ClientCommonConfig
|
||||
encryptionKey []byte
|
||||
msgTransporter transport.MessageTransporter
|
||||
@@ -106,6 +110,7 @@ func (pxy *BaseProxy) Run() error {
|
||||
if pxy.baseCfg.Plugin.Type != "" {
|
||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||
Name: pxy.baseCfg.Name,
|
||||
HostAllowList: pxy.getPluginHostAllowList(),
|
||||
VnetController: pxy.vnetController,
|
||||
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||
if err != nil {
|
||||
@@ -116,6 +121,39 @@ func (pxy *BaseProxy) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) getPluginHostAllowList() []string {
|
||||
dedupHosts := make([]string, 0)
|
||||
addHost := func(host string) {
|
||||
host = strings.TrimSpace(strings.ToLower(host))
|
||||
if host == "" {
|
||||
return
|
||||
}
|
||||
// autocert.HostWhitelist only supports exact host names.
|
||||
if strings.Contains(host, "*") {
|
||||
return
|
||||
}
|
||||
if !slices.Contains(dedupHosts, host) {
|
||||
dedupHosts = append(dedupHosts, host)
|
||||
}
|
||||
}
|
||||
|
||||
switch cfg := pxy.configurer.(type) {
|
||||
case *v1.HTTPProxyConfig:
|
||||
for _, host := range cfg.CustomDomains {
|
||||
addHost(host)
|
||||
}
|
||||
case *v1.HTTPSProxyConfig:
|
||||
for _, host := range cfg.CustomDomains {
|
||||
addHost(host)
|
||||
}
|
||||
case *v1.TCPMuxProxyConfig:
|
||||
for _, host := range cfg.CustomDomains {
|
||||
addHost(host)
|
||||
}
|
||||
}
|
||||
return dedupHosts
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) Close() {
|
||||
if pxy.proxyPlugin != nil {
|
||||
pxy.proxyPlugin.Close()
|
||||
|
||||
@@ -329,6 +329,14 @@ type = "https2http"
|
||||
localAddr = "127.0.0.1:80"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
||||
# [proxies.plugin.autoTLS]
|
||||
# enable = true
|
||||
# email = "admin@example.com"
|
||||
# cacheDir = "./.autotls-cache"
|
||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
||||
# hostAllowList = ["test.yourdomain.com"]
|
||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
@@ -341,6 +349,14 @@ type = "https2https"
|
||||
localAddr = "127.0.0.1:443"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
||||
# [proxies.plugin.autoTLS]
|
||||
# enable = true
|
||||
# email = "admin@example.com"
|
||||
# cacheDir = "./.autotls-cache"
|
||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
||||
# hostAllowList = ["test.yourdomain.com"]
|
||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
@@ -354,6 +370,15 @@ localAddr = "127.0.0.1:443"
|
||||
hostHeaderRewrite = "127.0.0.1"
|
||||
requestHeaders.set.x-from-where = "frp"
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http2https_redirect"
|
||||
type = "http"
|
||||
customDomains = ["test.yourdomain.com"]
|
||||
[proxies.plugin]
|
||||
type = "http2https_redirect"
|
||||
# Optional. Defaults to 443. Set this if the HTTPS entry is exposed on a non-standard port.
|
||||
# httpsPort = 443
|
||||
|
||||
[[proxies]]
|
||||
name = "plugin_http2http"
|
||||
type = "tcp"
|
||||
@@ -373,6 +398,14 @@ type = "tls2raw"
|
||||
localAddr = "127.0.0.1:80"
|
||||
crtPath = "./server.crt"
|
||||
keyPath = "./server.key"
|
||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
||||
# [proxies.plugin.autoTLS]
|
||||
# enable = true
|
||||
# email = "admin@example.com"
|
||||
# cacheDir = "./.autotls-cache"
|
||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
||||
# hostAllowList = ["test.yourdomain.com"]
|
||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
[[proxies]]
|
||||
name = "secret_tcp"
|
||||
|
||||
@@ -27,29 +27,31 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PluginHTTP2HTTPS = "http2https"
|
||||
PluginHTTPProxy = "http_proxy"
|
||||
PluginHTTPS2HTTP = "https2http"
|
||||
PluginHTTPS2HTTPS = "https2https"
|
||||
PluginHTTP2HTTP = "http2http"
|
||||
PluginSocks5 = "socks5"
|
||||
PluginStaticFile = "static_file"
|
||||
PluginUnixDomainSocket = "unix_domain_socket"
|
||||
PluginTLS2Raw = "tls2raw"
|
||||
PluginVirtualNet = "virtual_net"
|
||||
PluginHTTP2HTTPS = "http2https"
|
||||
PluginHTTP2HTTPSRedirect = "http2https_redirect"
|
||||
PluginHTTPProxy = "http_proxy"
|
||||
PluginHTTPS2HTTP = "https2http"
|
||||
PluginHTTPS2HTTPS = "https2https"
|
||||
PluginHTTP2HTTP = "http2http"
|
||||
PluginSocks5 = "socks5"
|
||||
PluginStaticFile = "static_file"
|
||||
PluginUnixDomainSocket = "unix_domain_socket"
|
||||
PluginTLS2Raw = "tls2raw"
|
||||
PluginVirtualNet = "virtual_net"
|
||||
)
|
||||
|
||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||
PluginHTTP2HTTPSRedirect: reflect.TypeOf(HTTP2HTTPSRedirectPluginOptions{}),
|
||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||
}
|
||||
|
||||
type ClientPluginOptions interface {
|
||||
@@ -109,6 +111,13 @@ type HTTP2HTTPSPluginOptions struct {
|
||||
|
||||
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
||||
|
||||
type HTTP2HTTPSRedirectPluginOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
HTTPSPort int `json:"httpsPort,omitempty"`
|
||||
}
|
||||
|
||||
func (o *HTTP2HTTPSRedirectPluginOptions) Complete() {}
|
||||
|
||||
type HTTPProxyPluginOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
HTTPUser string `json:"httpUser,omitempty"`
|
||||
@@ -117,6 +126,18 @@ type HTTPProxyPluginOptions struct {
|
||||
|
||||
func (o *HTTPProxyPluginOptions) Complete() {}
|
||||
|
||||
type AutoTLSOptions struct {
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
// Contact email for certificate expiration and important notices.
|
||||
Email string `json:"email,omitempty"`
|
||||
// Directory used to cache ACME account and certificates.
|
||||
CacheDir string `json:"cacheDir,omitempty"`
|
||||
// ACME directory URL, e.g. Let's Encrypt staging/prod endpoint.
|
||||
CADirURL string `json:"caDirURL,omitempty"`
|
||||
// Restrict certificate issuance to the listed domains.
|
||||
HostAllowList []string `json:"hostAllowList,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPS2HTTPPluginOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
LocalAddr string `json:"localAddr,omitempty"`
|
||||
@@ -125,6 +146,7 @@ type HTTPS2HTTPPluginOptions struct {
|
||||
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||
CrtPath string `json:"crtPath,omitempty"`
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
||||
}
|
||||
|
||||
func (o *HTTPS2HTTPPluginOptions) Complete() {
|
||||
@@ -139,6 +161,7 @@ type HTTPS2HTTPSPluginOptions struct {
|
||||
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||
CrtPath string `json:"crtPath,omitempty"`
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
||||
}
|
||||
|
||||
func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
||||
@@ -180,10 +203,11 @@ type UnixDomainSocketPluginOptions struct {
|
||||
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
||||
|
||||
type TLS2RawPluginOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
LocalAddr string `json:"localAddr,omitempty"`
|
||||
CrtPath string `json:"crtPath,omitempty"`
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
LocalAddr string `json:"localAddr,omitempty"`
|
||||
CrtPath string `json:"crtPath,omitempty"`
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
||||
}
|
||||
|
||||
func (o *TLS2RawPluginOptions) Complete() {}
|
||||
|
||||
@@ -16,6 +16,8 @@ package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
@@ -24,6 +26,8 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
|
||||
switch v := c.(type) {
|
||||
case *v1.HTTP2HTTPSPluginOptions:
|
||||
return validateHTTP2HTTPSPluginOptions(v)
|
||||
case *v1.HTTP2HTTPSRedirectPluginOptions:
|
||||
return validateHTTP2HTTPSRedirectPluginOptions(v)
|
||||
case *v1.HTTPS2HTTPPluginOptions:
|
||||
return validateHTTPS2HTTPPluginOptions(v)
|
||||
case *v1.HTTPS2HTTPSPluginOptions:
|
||||
@@ -45,10 +49,17 @@ func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHTTP2HTTPSRedirectPluginOptions(c *v1.HTTP2HTTPSRedirectPluginOptions) error {
|
||||
return ValidatePort(c.HTTPSPort, "httpsPort")
|
||||
}
|
||||
|
||||
func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error {
|
||||
if c.LocalAddr == "" {
|
||||
return errors.New("localAddr is required")
|
||||
}
|
||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,6 +67,9 @@ func validateHTTPS2HTTPSPluginOptions(c *v1.HTTPS2HTTPSPluginOptions) error {
|
||||
if c.LocalAddr == "" {
|
||||
return errors.New("localAddr is required")
|
||||
}
|
||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,5 +91,29 @@ func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error {
|
||||
if c.LocalAddr == "" {
|
||||
return errors.New("localAddr is required")
|
||||
}
|
||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAutoTLSOptions(c *v1.AutoTLSOptions, crtPath, keyPath string) error {
|
||||
if c == nil || !c.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if crtPath != "" || keyPath != "" {
|
||||
return errors.New("crtPath and keyPath must be empty when autoTLS.enable is true")
|
||||
}
|
||||
if strings.TrimSpace(c.CacheDir) == "" {
|
||||
return errors.New("autoTLS.cacheDir is required when autoTLS.enable is true")
|
||||
}
|
||||
if len(c.HostAllowList) > 0 {
|
||||
for _, host := range c.HostAllowList {
|
||||
if strings.TrimSpace(host) == "" {
|
||||
return errors.New("autoTLS.hostAllowList cannot contain empty domain")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
212
pkg/plugin/client/autotls.go
Normal file
212
pkg/plugin/client/autotls.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright 2026 The LoliaTeam 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.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
)
|
||||
|
||||
func buildAutoTLSServerConfigWithHosts(pluginName string, auto *v1.AutoTLSOptions, fallbackHosts []string) (*tls.Config, error) {
|
||||
if auto == nil || !auto.Enable {
|
||||
return nil, fmt.Errorf("插件 %s 未启用 autoTLS", pluginName)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(auto.CacheDir, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("插件 %s 创建 autoTLS 缓存目录失败: %w", pluginName, err)
|
||||
}
|
||||
|
||||
hostSet := make(map[string]struct{})
|
||||
hosts := make([]string, 0, len(auto.HostAllowList))
|
||||
addHost := func(host string) {
|
||||
host = strings.TrimSpace(strings.ToLower(host))
|
||||
if host == "" {
|
||||
return
|
||||
}
|
||||
if strings.Contains(host, "*") {
|
||||
log.Warnf("[autoTLS][%s] 域名 [%s] 含通配符,自动申请不支持,已忽略", pluginName, host)
|
||||
return
|
||||
}
|
||||
if _, ok := hostSet[host]; ok {
|
||||
return
|
||||
}
|
||||
hostSet[host] = struct{}{}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
for _, host := range auto.HostAllowList {
|
||||
addHost(host)
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
for _, host := range fallbackHosts {
|
||||
addHost(host)
|
||||
}
|
||||
}
|
||||
if len(hosts) == 0 {
|
||||
return nil, fmt.Errorf("插件 %s 的 hostAllowList 为空;请设置 autoTLS.hostAllowList 或 customDomains", pluginName)
|
||||
}
|
||||
|
||||
manager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: strings.TrimSpace(auto.Email),
|
||||
HostPolicy: autocert.HostWhitelist(hosts...),
|
||||
}
|
||||
caDirURL := strings.TrimSpace(auto.CADirURL)
|
||||
if caDirURL != "" {
|
||||
manager.Client = &acme.Client{DirectoryURL: caDirURL}
|
||||
} else {
|
||||
caDirURL = autocert.DefaultACMEDirectory
|
||||
}
|
||||
managedHosts := make(map[string]struct{}, len(hosts))
|
||||
for _, host := range hosts {
|
||||
managedHosts[host] = struct{}{}
|
||||
}
|
||||
var warmupInProgress sync.Map
|
||||
var warmupMissLogged sync.Map
|
||||
manager.Cache = &autoTLSCache{
|
||||
inner: autocert.DirCache(auto.CacheDir),
|
||||
managedHosts: managedHosts,
|
||||
pluginName: pluginName,
|
||||
caDirURL: caDirURL,
|
||||
warmupInProgress: &warmupInProgress,
|
||||
warmupMissLogged: &warmupMissLogged,
|
||||
}
|
||||
|
||||
cfg := manager.TLSConfig()
|
||||
log.Infof("[autoTLS][%s] 已启用 autoTLS,管理域名=%v,缓存目录=%s", pluginName, hosts, auto.CacheDir)
|
||||
|
||||
var readySeen sync.Map
|
||||
|
||||
handleCertReady := func(host string, cert *tls.Certificate) {
|
||||
var (
|
||||
notAfter time.Time
|
||||
hasExpiry bool
|
||||
)
|
||||
if t, ok := getCertificateNotAfter(cert); ok {
|
||||
notAfter = t
|
||||
hasExpiry = true
|
||||
}
|
||||
|
||||
_, readyLogged := readySeen.LoadOrStore(host, struct{}{})
|
||||
if hasExpiry {
|
||||
if !readyLogged {
|
||||
log.Infof("[autoTLS][%s] 域名 [%s] 证书已就绪,过期时间 %s", pluginName, host, notAfter.Format(time.RFC3339))
|
||||
}
|
||||
} else if !readyLogged {
|
||||
log.Infof("[autoTLS][%s] 域名 [%s] 证书已就绪", pluginName, host)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
host := strings.TrimSpace(strings.ToLower(hello.ServerName))
|
||||
if host == "" {
|
||||
host = "<空SNI>"
|
||||
}
|
||||
|
||||
cert, err := manager.GetCertificate(hello)
|
||||
if err != nil {
|
||||
log.Warnf("[autoTLS][%s] 获取域名 [%s] 证书失败: %v", pluginName, host, err)
|
||||
return nil, err
|
||||
}
|
||||
handleCertReady(host, cert)
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// Warm up certificates in background after startup.
|
||||
for _, host := range hosts {
|
||||
h := host
|
||||
go func() {
|
||||
// Leave time for listener setup and route registration.
|
||||
time.Sleep(1 * time.Second)
|
||||
warmupMissLogged.Delete(h)
|
||||
warmupInProgress.Store(h, struct{}{})
|
||||
cert, err := manager.GetCertificate(&tls.ClientHelloInfo{ServerName: h})
|
||||
warmupInProgress.Delete(h)
|
||||
if err != nil {
|
||||
log.Warnf("[autoTLS][%s] 域名 [%s] 预申请失败: %v", pluginName, h, err)
|
||||
return
|
||||
}
|
||||
handleCertReady(h, cert)
|
||||
}()
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getCertificateNotAfter(cert *tls.Certificate) (time.Time, bool) {
|
||||
if cert == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
if cert.Leaf != nil {
|
||||
return cert.Leaf.NotAfter, true
|
||||
}
|
||||
if len(cert.Certificate) == 0 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return leaf.NotAfter, true
|
||||
}
|
||||
|
||||
type autoTLSCache struct {
|
||||
inner autocert.Cache
|
||||
managedHosts map[string]struct{}
|
||||
pluginName string
|
||||
caDirURL string
|
||||
warmupInProgress *sync.Map
|
||||
warmupMissLogged *sync.Map
|
||||
}
|
||||
|
||||
func (c *autoTLSCache) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
data, err := c.inner.Get(ctx, key)
|
||||
if err != autocert.ErrCacheMiss {
|
||||
return data, err
|
||||
}
|
||||
|
||||
host := strings.TrimSuffix(key, "+rsa")
|
||||
if _, ok := c.managedHosts[host]; !ok {
|
||||
return data, err
|
||||
}
|
||||
if _, warming := c.warmupInProgress.Load(host); !warming {
|
||||
return data, err
|
||||
}
|
||||
if _, loaded := c.warmupMissLogged.LoadOrStore(host, struct{}{}); !loaded {
|
||||
log.Infof("[autoTLS][%s] 开始预申请域名 [%s] 证书,申请方式=TLS-ALPN-01,caDirURL=%s", c.pluginName, host, c.caDirURL)
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (c *autoTLSCache) Put(ctx context.Context, key string, data []byte) error {
|
||||
return c.inner.Put(ctx, key, data)
|
||||
}
|
||||
|
||||
func (c *autoTLSCache) Delete(ctx context.Context, key string) error {
|
||||
return c.inner.Delete(ctx, key)
|
||||
}
|
||||
109
pkg/plugin/client/http2https_redirect.go
Normal file
109
pkg/plugin/client/http2https_redirect.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2026 The LoliaTeam 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.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(v1.PluginHTTP2HTTPSRedirect, NewHTTP2HTTPSRedirectPlugin)
|
||||
}
|
||||
|
||||
type HTTP2HTTPSRedirectPlugin struct {
|
||||
opts *v1.HTTP2HTTPSRedirectPluginOptions
|
||||
|
||||
l *Listener
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
func NewHTTP2HTTPSRedirectPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
opts := options.(*v1.HTTP2HTTPSRedirectPluginOptions)
|
||||
|
||||
listener := NewProxyListener()
|
||||
p := &HTTP2HTTPSRedirectPlugin{
|
||||
opts: opts,
|
||||
l: listener,
|
||||
}
|
||||
|
||||
p.s = &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, buildHTTPSRedirectURL(req, opts.HTTPSPort), http.StatusFound)
|
||||
}),
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = p.s.Serve(listener)
|
||||
}()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func buildHTTPSRedirectURL(req *http.Request, httpsPort int) string {
|
||||
host := strings.TrimSpace(req.Host)
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(req.URL.Host)
|
||||
}
|
||||
|
||||
targetHost := host
|
||||
if parsedHost, parsedPort, err := net.SplitHostPort(host); err == nil {
|
||||
targetHost = parsedHost
|
||||
if httpsPort == 0 && parsedPort == "443" {
|
||||
httpsPort = 443
|
||||
}
|
||||
} else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
targetHost = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]")
|
||||
}
|
||||
|
||||
if httpsPort != 0 && httpsPort != 443 {
|
||||
targetHost = net.JoinHostPort(targetHost, strconv.Itoa(httpsPort))
|
||||
}
|
||||
|
||||
return (&url.URL{
|
||||
Scheme: "https",
|
||||
Host: targetHost,
|
||||
Path: req.URL.Path,
|
||||
RawPath: req.URL.RawPath,
|
||||
RawQuery: req.URL.RawQuery,
|
||||
}).String()
|
||||
}
|
||||
|
||||
func (p *HTTP2HTTPSRedirectPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
||||
wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
|
||||
if connInfo.SrcAddr != nil {
|
||||
wrapConn.SetRemoteAddr(connInfo.SrcAddr)
|
||||
}
|
||||
_ = p.l.PutConn(wrapConn)
|
||||
}
|
||||
|
||||
func (p *HTTP2HTTPSRedirectPlugin) Name() string {
|
||||
return v1.PluginHTTP2HTTPSRedirect
|
||||
}
|
||||
|
||||
func (p *HTTP2HTTPSRedirectPlugin) Close() error {
|
||||
return p.s.Close()
|
||||
}
|
||||
@@ -46,7 +46,7 @@ type HTTPS2HTTPPlugin struct {
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
func NewHTTPS2HTTPPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
opts := options.(*v1.HTTPS2HTTPPluginOptions)
|
||||
listener := NewProxyListener()
|
||||
|
||||
@@ -84,9 +84,18 @@ func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi
|
||||
rp.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build autoTLS config error: %v", err)
|
||||
}
|
||||
} else {
|
||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
p.s = &http.Server{
|
||||
|
||||
@@ -46,7 +46,7 @@ type HTTPS2HTTPSPlugin struct {
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
func NewHTTPS2HTTPSPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
opts := options.(*v1.HTTPS2HTTPSPluginOptions)
|
||||
|
||||
listener := NewProxyListener()
|
||||
@@ -90,9 +90,18 @@ func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plug
|
||||
rp.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build autoTLS config error: %v", err)
|
||||
}
|
||||
} else {
|
||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
p.s = &http.Server{
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
type PluginContext struct {
|
||||
Name string
|
||||
HostAllowList []string
|
||||
VnetController *vnet.Controller
|
||||
}
|
||||
|
||||
|
||||
@@ -39,16 +39,25 @@ type TLS2RawPlugin struct {
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func NewTLS2RawPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
func NewTLS2RawPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||
opts := options.(*v1.TLS2RawPluginOptions)
|
||||
|
||||
p := &TLS2RawPlugin{
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var tlsConfig *tls.Config
|
||||
var err error
|
||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.tlsConfig = tlsConfig
|
||||
return p, nil
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
package version
|
||||
|
||||
var version = "LoliaFRP-CLI 0.67.0"
|
||||
var version = "LoliaFRP-CLI 0.67.4"
|
||||
|
||||
func Full() string {
|
||||
return version
|
||||
|
||||
@@ -38,21 +38,28 @@ type Controller struct {
|
||||
serverCfg *v1.ServerConfig
|
||||
clientRegistry *registry.ClientRegistry
|
||||
pxyManager ProxyManager
|
||||
ctlManager ControlManager
|
||||
}
|
||||
|
||||
type ProxyManager interface {
|
||||
GetByName(name string) (proxy.Proxy, bool)
|
||||
}
|
||||
|
||||
type ControlManager interface {
|
||||
CloseAllProxyByName(proxyName string) error
|
||||
}
|
||||
|
||||
func NewController(
|
||||
serverCfg *v1.ServerConfig,
|
||||
clientRegistry *registry.ClientRegistry,
|
||||
pxyManager ProxyManager,
|
||||
ctlManager ControlManager,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
serverCfg: serverCfg,
|
||||
clientRegistry: clientRegistry,
|
||||
pxyManager: pxyManager,
|
||||
ctlManager: ctlManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +253,24 @@ func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
|
||||
return proxyInfo, nil
|
||||
}
|
||||
|
||||
// POST /api/proxy/:name/close
|
||||
func (c *Controller) APICloseProxyByName(ctx *httppkg.Context) (any, error) {
|
||||
name := ctx.Param("name")
|
||||
if name == "" {
|
||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name required")
|
||||
}
|
||||
|
||||
if c.ctlManager == nil {
|
||||
return nil, fmt.Errorf("control manager unavailable")
|
||||
}
|
||||
|
||||
if err := c.ctlManager.CloseAllProxyByName(name); err != nil {
|
||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return httppkg.GeneralResponse{Code: 200, Msg: "ok"}, nil
|
||||
}
|
||||
|
||||
// DELETE /api/proxies?status=offline
|
||||
func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
|
||||
status := ctx.Query("status")
|
||||
|
||||
@@ -181,18 +181,26 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
||||
})
|
||||
}
|
||||
|
||||
name := pxy.GetName()
|
||||
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
||||
rwc = wrapCountingReadWriteCloser(rwc, func(bytes int64) {
|
||||
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
||||
}, func(bytes int64) {
|
||||
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
||||
})
|
||||
|
||||
workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
|
||||
workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
|
||||
metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||
workConn = netpkg.WrapCloseNotifyConn(workConn, func(error) {
|
||||
pxy.updateStatsAfterClosedConn()
|
||||
})
|
||||
metrics.Server.OpenConnection(name, proxyType)
|
||||
return
|
||||
}
|
||||
|
||||
func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) {
|
||||
func (pxy *HTTPProxy) updateStatsAfterClosedConn() {
|
||||
name := pxy.GetName()
|
||||
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
||||
metrics.Server.CloseConnection(name, proxyType)
|
||||
metrics.Server.AddTrafficIn(name, proxyType, totalWrite)
|
||||
metrics.Server.AddTrafficOut(name, proxyType, totalRead)
|
||||
}
|
||||
|
||||
func (pxy *HTTPProxy) Close() {
|
||||
|
||||
@@ -263,11 +263,18 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
||||
|
||||
name := pxy.GetName()
|
||||
proxyType := cfg.Type
|
||||
local = wrapCountingReadWriteCloser(local, nil, func(bytes int64) {
|
||||
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
||||
})
|
||||
userConn = netpkg.WrapReadWriteCloserToConn(
|
||||
wrapCountingReadWriteCloser(userConn, nil, func(bytes int64) {
|
||||
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
||||
}),
|
||||
userConn,
|
||||
)
|
||||
metrics.Server.OpenConnection(name, proxyType)
|
||||
inCount, outCount, _ := libio.Join(local, userConn)
|
||||
_, _, _ = libio.Join(local, userConn)
|
||||
metrics.Server.CloseConnection(name, proxyType)
|
||||
metrics.Server.AddTrafficIn(name, proxyType, inCount)
|
||||
metrics.Server.AddTrafficOut(name, proxyType, outCount)
|
||||
xl.Debugf("join connections closed")
|
||||
}
|
||||
|
||||
|
||||
36
server/proxy/traffic_counter.go
Normal file
36
server/proxy/traffic_counter.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package proxy
|
||||
|
||||
import "io"
|
||||
|
||||
type countingReadWriteCloser struct {
|
||||
io.ReadWriteCloser
|
||||
onRead func(int64)
|
||||
onWrite func(int64)
|
||||
}
|
||||
|
||||
func wrapCountingReadWriteCloser(rwc io.ReadWriteCloser, onRead, onWrite func(int64)) io.ReadWriteCloser {
|
||||
if onRead == nil && onWrite == nil {
|
||||
return rwc
|
||||
}
|
||||
return &countingReadWriteCloser{
|
||||
ReadWriteCloser: rwc,
|
||||
onRead: onRead,
|
||||
onWrite: onWrite,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *countingReadWriteCloser) Read(p []byte) (n int, err error) {
|
||||
n, err = c.ReadWriteCloser.Read(p)
|
||||
if n > 0 && c.onRead != nil {
|
||||
c.onRead(int64(n))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *countingReadWriteCloser) Write(p []byte) (n int, err error) {
|
||||
n, err = c.ReadWriteCloser.Write(p)
|
||||
if n > 0 && c.onWrite != nil {
|
||||
c.onWrite(int64(n))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -703,12 +703,13 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
||||
subRouter.Handle("/metrics", promhttp.Handler())
|
||||
}
|
||||
|
||||
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
|
||||
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager, svr.ctlManager)
|
||||
|
||||
// apis
|
||||
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
||||
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
||||
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
||||
subRouter.HandleFunc("/api/proxy/{name}/close", httppkg.MakeHTTPHandlerFunc(apiController.APICloseProxyByName)).Methods("POST")
|
||||
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
||||
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
||||
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
||||
|
||||
22
web/frpc/package-lock.json
generated
22
web/frpc/package-lock.json
generated
@@ -1446,6 +1446,7 @@
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -1454,6 +1455,7 @@
|
||||
"version": "24.10.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1506,6 +1508,7 @@
|
||||
"version": "6.20.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.20.0",
|
||||
"@typescript-eslint/types": "6.20.0",
|
||||
@@ -1906,6 +1909,7 @@
|
||||
"version": "14.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.1.0",
|
||||
@@ -1941,6 +1945,7 @@
|
||||
"version": "8.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2789,6 +2794,7 @@
|
||||
"version": "8.56.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -2843,6 +2849,7 @@
|
||||
"version": "9.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -2883,6 +2890,7 @@
|
||||
"version": "9.33.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"globals": "^13.24.0",
|
||||
@@ -3970,13 +3978,15 @@
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -4521,6 +4531,7 @@
|
||||
"version": "3.7.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -4683,6 +4694,7 @@
|
||||
"version": "4.55.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -5074,6 +5086,7 @@
|
||||
"version": "5.44.1",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -5135,6 +5148,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5210,6 +5224,7 @@
|
||||
"version": "5.9.3",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5609,6 +5624,7 @@
|
||||
"version": "7.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -5721,6 +5737,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5736,6 +5753,7 @@
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.26",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
|
||||
20
web/frps/package-lock.json
generated
20
web/frps/package-lock.json
generated
@@ -1556,6 +1556,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -1566,6 +1567,7 @@
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1626,6 +1628,7 @@
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
@@ -2038,6 +2041,7 @@
|
||||
"integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.1.0",
|
||||
@@ -2079,6 +2083,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3039,6 +3044,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -3095,6 +3101,7 @@
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -3139,6 +3146,7 @@
|
||||
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"globals": "^13.24.0",
|
||||
@@ -4513,13 +4521,15 @@
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -5145,6 +5155,7 @@
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -5354,6 +5365,7 @@
|
||||
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -5795,6 +5807,7 @@
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -5922,6 +5935,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -6369,6 +6383,7 @@
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -6463,6 +6478,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
|
||||
Reference in New Issue
Block a user