From d63f7fd0e4439482704302ea977f2f4442770155 Mon Sep 17 00:00:00 2001
From: Muhammad Nasrul
Date: Mon, 8 Sep 2025 15:48:59 +0700
Subject: [PATCH 1/3] [ADD] feature vHost customResponse
---
pkg/config/v1/server.go | 15 +++++
pkg/util/http/http.go | 23 ++++++++
pkg/util/vhost/http.go | 5 ++
pkg/util/vhost/resource.go | 114 +++++++++++++++++++++++++++++++++++++
server/service.go | 20 +++++++
5 files changed, 177 insertions(+)
diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go
index 54aac080..11e397e0 100644
--- a/pkg/config/v1/server.go
+++ b/pkg/config/v1/server.go
@@ -99,6 +99,8 @@ type ServerConfig struct {
AllowPorts []types.PortsRange `json:"allowPorts,omitempty"`
HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"`
+
+ CustomResponse *CustomResponseConfig `json:"customResponse,omitempty" toml:"customResponse" yaml:"customResponse"`
}
func (c *ServerConfig) Complete() error {
@@ -227,3 +229,16 @@ type SSHTunnelGateway struct {
func (c *SSHTunnelGateway) Complete() {
c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
}
+
+type CustomResponseConfig struct {
+ Enable bool `toml:"enable" json:"enable" yaml:"enable"`
+ Rules []CustomResponseRule `toml:"rules" json:"rules" yaml:"rules"`
+}
+
+type CustomResponseRule struct {
+ Hostname []string `toml:"hostname" json:"hostname" yaml:"hostname"`
+ StatusCode int `toml:"statusCode" json:"statusCode" yaml:"statusCode"`
+ ContentType string `toml:"contentType" json:"contentType" yaml:"contentType"`
+ Body string `toml:"body" json:"body" yaml:"body"`
+ Headers map[string]string `toml:"headers" json:"headers" yaml:"headers"`
+}
diff --git a/pkg/util/http/http.go b/pkg/util/http/http.go
index b85a46a3..e4c2f5b6 100644
--- a/pkg/util/http/http.go
+++ b/pkg/util/http/http.go
@@ -100,3 +100,26 @@ func BasicAuth(username, passwd string) string {
auth := username + ":" + passwd
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}
+
+// MatchDomain checks if the request host matches the config host.
+// The config host can be a wildcard domain like "*.example.com".
+func MatchDomain(requestHost, configHost string) bool {
+ if len(requestHost) == 0 || len(configHost) == 0 {
+ return false
+ }
+ if configHost == "*" {
+ return true
+ }
+ if requestHost == configHost {
+ return true
+ }
+ if strings.HasPrefix(configHost, "*.") {
+ // The config host is a wildcard domain like "*.example.com".
+ // We need to check if the request host ends with the config host.
+ suffix := configHost[1:] // Remove '*'
+ if strings.HasSuffix(requestHost, suffix) && len(requestHost) > len(suffix) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go
index 05ec174b..de26ca3f 100644
--- a/pkg/util/vhost/http.go
+++ b/pkg/util/vhost/http.go
@@ -40,6 +40,7 @@ var ErrNoRouteFound = errors.New("no route found")
type HTTPReverseProxyOptions struct {
ResponseHeaderTimeoutS int64
+ CustomErrorPage *CustomErrorPage
}
type HTTPReverseProxy struct {
@@ -136,6 +137,10 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
return
}
}
+ customResponseStatus := CustomErrorResponse(rw, req, option.CustomErrorPage)
+ if customResponseStatus {
+ return
+ }
rw.WriteHeader(http.StatusNotFound)
_, _ = rw.Write(getNotFoundPageContent())
},
diff --git a/pkg/util/vhost/resource.go b/pkg/util/vhost/resource.go
index a65e2997..5c55777a 100644
--- a/pkg/util/vhost/resource.go
+++ b/pkg/util/vhost/resource.go
@@ -20,11 +20,26 @@ import (
"net/http"
"os"
+ httppkg "github.com/fatedier/frp/pkg/util/http"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
+type CustomErrorPage struct {
+ Enable bool
+ Rules []CustomResponseRule
+}
+
+type CustomResponseRule struct {
+ Hostname []string
+ StatusCode int
+ ContentType string
+ Body string
+ Headers map[string]string
+}
+
var NotFoundPagePath = ""
+var ServiceUnavailablePagePath = ""
const (
NotFound = `
@@ -47,6 +62,27 @@ Please try again later.
Faithfully yours, frp.