- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips) - Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF' - Detection engine enhancement: strict preset enables all 17 modules with aggressive configs - Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA - Rate limiting: nginx limit_req per website (RPS, burst, action) - JS Challenge: browser fingerprint verification page with cookie-based bypass - Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/model"
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/pkg/ngcmd"
|
||||
pb "chaitin.cn/patronus/safeline-2/management/tcontrollerd/proto/website"
|
||||
"chaitin.cn/patronus/safeline-2/management/tcontrollerd/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
Ping = "ping"
|
||||
Pong = "pong"
|
||||
EventTypeWebsite = "website"
|
||||
EventTypeDeleteWebsite = "deleteWebsite"
|
||||
EventTypeFullWebsite = "fullWebsite"
|
||||
|
||||
nginxConfigPath = "/etc/nginx/sites-enabled/" // 修改的为 host /resources/nginx/ 中的文件
|
||||
nginxFilePrefix = "IF_backend_"
|
||||
nginxBackupFilePrefix = "BAK_IF_backend_"
|
||||
nginxFileMode = 0644
|
||||
|
||||
HttpsScheme = "https"
|
||||
DefaultHttpsPort = "443"
|
||||
)
|
||||
|
||||
var pong = pb.Response{Type: Pong, Msg: nil, Err: false}
|
||||
|
||||
func generateNginxConfig(website *model.WebsiteConfig) (string, error) {
|
||||
// only ONE upstream supported in v1.0
|
||||
var upstreamAddr string
|
||||
scheme := "http"
|
||||
for _, upstream := range website.Upstreams {
|
||||
urlInfo, err := url.Parse(upstream)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if urlInfo.Scheme == HttpsScheme && urlInfo.Port() == "" {
|
||||
urlInfo.Host = urlInfo.Host + ":" + DefaultHttpsPort
|
||||
}
|
||||
upstreamAddr = fmt.Sprintf(upstreamAddrTpl, urlInfo.Host)
|
||||
if len(urlInfo.Scheme) > 0 {
|
||||
scheme = urlInfo.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
sslFlag := ""
|
||||
sslCertFilename := ""
|
||||
sslCertKeyFilename := ""
|
||||
if website.KeyFilename != "" && website.CertFilename != "" {
|
||||
sslFlag = " ssl" // with a blank ahead
|
||||
sslCertFilename = fmt.Sprintf(certTpl, website.CertFilename)
|
||||
sslCertKeyFilename = fmt.Sprintf(certKeyTpl, website.KeyFilename)
|
||||
}
|
||||
|
||||
// only ONE server name supported in v1.0
|
||||
var serverName string
|
||||
var addrAnyProperties string
|
||||
for _, sn := range website.ServerNames {
|
||||
if sn == "*" || sn == "" {
|
||||
sn = "_"
|
||||
addrAnyProperties = addrAnyPropertiesTpl
|
||||
}
|
||||
serverName = fmt.Sprintf(serverNameTpl, sn)
|
||||
}
|
||||
|
||||
// only ONE port supported in v1.0
|
||||
var serverListen string
|
||||
for _, port := range website.Ports {
|
||||
serverListen = fmt.Sprintf(serverListenTpl, port, sslFlag, addrAnyProperties)
|
||||
}
|
||||
|
||||
// Rate limiting directives
|
||||
rateLimitDirective := ""
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
burst := website.RateLimitBurst
|
||||
if burst <= 0 {
|
||||
burst = rps * 2
|
||||
}
|
||||
rateLimitDirective = fmt.Sprintf(rateLimitReqTpl, zoneName, burst)
|
||||
}
|
||||
|
||||
// Challenge directive
|
||||
challengeDirective := ""
|
||||
if website.ChallengeEnabled {
|
||||
challengeDirective = `error_page 403 = /__waf_challenge;`
|
||||
}
|
||||
|
||||
upstreamName := fmt.Sprintf(backendTpl, website.Id)
|
||||
nginxConfig := strings.TrimSpace(fmt.Sprintf(nginxConfigTpl,
|
||||
upstreamName, upstreamAddr,
|
||||
serverListen, serverName, sslCertFilename, sslCertKeyFilename,
|
||||
scheme, upstreamName, website.Id,
|
||||
rateLimitDirective, challengeDirective,
|
||||
))
|
||||
|
||||
// Prepend rate limit zone definition if enabled
|
||||
if website.RateLimitEnabled && website.RateLimitRPS > 0 {
|
||||
zoneName := fmt.Sprintf("site_%d", website.Id)
|
||||
rps := website.RateLimitRPS
|
||||
zoneDef := fmt.Sprintf(rateLimitZoneTpl, zoneName, 10, fmt.Sprintf("%dr/s", rps))
|
||||
nginxConfig = zoneDef + "\n" + nginxConfig
|
||||
}
|
||||
|
||||
return nginxConfig, nil
|
||||
}
|
||||
|
||||
func nginxTestAndReload() error {
|
||||
err := ngcmd.NginxConfTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ngcmd.NginxConfReload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateFullConfigAndReload(msg []byte) error {
|
||||
var websites []model.WebsiteConfig
|
||||
if err := json.Unmarshal(msg, &websites); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFilename := make(map[string]struct{})
|
||||
for _, website := range websites {
|
||||
configFilename[fmt.Sprintf("%s%d", nginxFilePrefix, website.Id)] = struct{}{}
|
||||
}
|
||||
filepath.Walk(nginxConfigPath, func(path string, info fs.FileInfo, err error) error {
|
||||
_, ok := configFilename[info.Name()]
|
||||
if !ok && strings.HasPrefix(info.Name(), nginxFilePrefix) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
// not return error only logged error in order to delete not exist website nginx conf
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, website := range websites {
|
||||
if err := generateConfigAndReload(&website); err != nil {
|
||||
// trigger a full site push when tcd starts, ignore some site errors, and push as much as possible
|
||||
logger.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateConfigAndReload(website *model.WebsiteConfig) error {
|
||||
nginxConfig, err := generateNginxConfig(website)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, website.Id))
|
||||
backupPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxBackupFilePrefix, website.Id))
|
||||
|
||||
oldConfigExists, err := utils.FileExist(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldConfigExists {
|
||||
oldConfig, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(oldConfig) == nginxConfig {
|
||||
logger.Info("No changes in the new website config, skip nginx -s reload")
|
||||
return nil
|
||||
}
|
||||
|
||||
// tmp save old config to the backup path
|
||||
if err = utils.CopyFile(configPath, backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = utils.EnsureWriteFile(configPath, []byte(nginxConfig), nginxFileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = nginxTestAndReload(); err != nil {
|
||||
nginxError := err
|
||||
if err = os.Remove(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if oldConfigExists {
|
||||
// new config err, restore old config
|
||||
if err = utils.CopyFile(backupPath, configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nginxError
|
||||
}
|
||||
|
||||
if oldConfigExists {
|
||||
if err = os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteConfigAndReload(config []byte) error {
|
||||
var websiteIds []uint
|
||||
if err := json.Unmarshal(config, &websiteIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range websiteIds {
|
||||
configPath := filepath.Join(nginxConfigPath, fmt.Sprintf("%s%d", nginxFilePrefix, id))
|
||||
exists, err := utils.FileExist(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := nginxTestAndReload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendResponse(stream pb.Website_SubscribeClient, eventType string, errMsg []byte) error {
|
||||
return stream.Send(&pb.Response{
|
||||
Type: eventType,
|
||||
Msg: errMsg,
|
||||
Err: len(errMsg) != 0,
|
||||
})
|
||||
}
|
||||
|
||||
func websiteHandler(wc pb.WebsiteClient) error {
|
||||
stream, err := wc.Subscribe(context.Background())
|
||||
if err != nil {
|
||||
logger.Errorf("Subscribe failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
event, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// read done.
|
||||
logger.Infof("Recv EOF from webserver")
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("Handle failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Debugf("Got message Type %s, Msg: %s", event.GetType(), event.GetMsg())
|
||||
if event.Type == Ping {
|
||||
if err = stream.Send(&pong); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if event.Type == EventTypeWebsite {
|
||||
logger.Infof("Update website with config: %s", event.GetMsg())
|
||||
var website model.WebsiteConfig
|
||||
if err := json.Unmarshal(event.GetMsg(), &website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = generateConfigAndReload(&website); err != nil {
|
||||
logger.Error(err)
|
||||
if err := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if event.Type == EventTypeFullWebsite {
|
||||
if err = generateFullConfigAndReload(event.Msg); err != nil {
|
||||
logger.Error(err)
|
||||
sendErr := sendResponse(stream, EventTypeFullWebsite, []byte(err.Error()))
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeFullWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if event.Type == EventTypeDeleteWebsite {
|
||||
if err = deleteConfigAndReload(event.Msg); err != nil {
|
||||
logger.Error(err)
|
||||
sendErr := sendResponse(stream, EventTypeDeleteWebsite, []byte(err.Error()))
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
} else {
|
||||
if err = sendResponse(stream, EventTypeDeleteWebsite, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user