mirror of https://github.com/fatedier/frp
[ADD] feature vHost customResponse
parent
0a798a7a69
commit
d63f7fd0e4
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
},
|
||||
|
|
|
@ -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 = `<!DOCTYPE html>
|
||||
|
@ -47,6 +62,27 @@ Please try again later.</p>
|
|||
<p><em>Faithfully yours, frp.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
ServerUnavailable = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Service Unavailable</title>
|
||||
<style>
|
||||
body {
|
||||
width: 35em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hostname not found.</h1>
|
||||
<p>Sorry, the page you are looking for is currently unavailable.<br/>
|
||||
Please try again later.</p>
|
||||
<p>The server is powered by <a href="https://github.com/fatedier/frp">frp</a>.</p>
|
||||
<p><em>Faithfully yours, frp.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -85,3 +121,81 @@ func NotFoundResponse() *http.Response {
|
|||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getServerUnavailablePageContent() []byte {
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
)
|
||||
if ServiceUnavailablePagePath != "" {
|
||||
buf, err = os.ReadFile(ServiceUnavailablePagePath)
|
||||
if err != nil {
|
||||
log.Warnf("read custom 404 page error: %v", err)
|
||||
buf = []byte(ServerUnavailable)
|
||||
}
|
||||
} else {
|
||||
buf = []byte(ServerUnavailable)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
func ServerUnavailableResponse() *http.Response {
|
||||
header := make(http.Header)
|
||||
header.Set("server", "frp/"+version.Full())
|
||||
header.Set("Content-Type", "text/html")
|
||||
header.Set("Frp-Custom-Error", "true")
|
||||
|
||||
content := []byte("Service Unavailable")
|
||||
res := &http.Response{
|
||||
Status: "Service Unavailable",
|
||||
StatusCode: 503,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: header,
|
||||
Body: io.NopCloser(bytes.NewReader(content)),
|
||||
ContentLength: int64(len(content)),
|
||||
}
|
||||
return res
|
||||
}
|
||||
func CustomErrorResponse(rw http.ResponseWriter, r *http.Request, option *CustomErrorPage) bool {
|
||||
if option == nil || !option.Enable {
|
||||
return false
|
||||
}
|
||||
host, err := httppkg.CanonicalHost(r.Host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, rule := range option.Rules {
|
||||
for _, pat := range rule.Hostname {
|
||||
if pat == "" {
|
||||
continue
|
||||
}
|
||||
if httppkg.MatchDomain(host, pat) {
|
||||
for k, v := range rule.Headers {
|
||||
rw.Header().Set(k, v)
|
||||
}
|
||||
if rule.ContentType != "" {
|
||||
rw.Header().Set("Content-Type", rule.ContentType)
|
||||
} else {
|
||||
rw.Header().Set("Content-Type", "text/html")
|
||||
}
|
||||
if rw.Header().Get("Cache-Control") == "" {
|
||||
rw.Header().Set("Cache-Control", "no-store")
|
||||
}
|
||||
code := rule.StatusCode
|
||||
if code == 0 {
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
rw.WriteHeader(code)
|
||||
if rule.Body != "" {
|
||||
_, _ = rw.Write([]byte(rule.Body))
|
||||
} else {
|
||||
_, _ = rw.Write(getServerUnavailablePageContent())
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false // fallback ke 404 default
|
||||
}
|
||||
|
|
|
@ -278,8 +278,28 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||
|
||||
// Create http vhost muxer.
|
||||
if cfg.VhostHTTPPort > 0 {
|
||||
var customErrorPage *vhost.CustomErrorPage
|
||||
if cfg.CustomResponse != nil && cfg.CustomResponse.Enable {
|
||||
rules := make([]vhost.CustomResponseRule, 0, len(cfg.CustomResponse.Rules))
|
||||
for _, r := range cfg.CustomResponse.Rules {
|
||||
rules = append(rules, vhost.CustomResponseRule{
|
||||
Hostname: r.Hostname,
|
||||
StatusCode: r.StatusCode,
|
||||
ContentType: r.ContentType,
|
||||
Body: r.Body,
|
||||
Headers: r.Headers,
|
||||
})
|
||||
}
|
||||
customErrorPage = &vhost.CustomErrorPage{
|
||||
Enable: cfg.CustomResponse.Enable,
|
||||
Rules: rules,
|
||||
}
|
||||
log.Infof("custom response rules loaded: %v", rules)
|
||||
log.Infof("custom response is enabled")
|
||||
}
|
||||
rp := vhost.NewHTTPReverseProxy(vhost.HTTPReverseProxyOptions{
|
||||
ResponseHeaderTimeoutS: cfg.VhostHTTPTimeout,
|
||||
CustomErrorPage: customErrorPage,
|
||||
}, svr.httpVhostRouter)
|
||||
svr.rc.HTTPReverseProxy = rp
|
||||
|
||||
|
|
Loading…
Reference in New Issue