lucky/module/reverseproxy/conf/reverseproxy.go

522 lines
13 KiB
Go

package reverseproxyconf
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"sync"
"time"
//"github.com/gdy666/lucky/module/safe"
//ssl "github.com/gdy666/lucky/module/sslcertficate"
"github.com/gdy666/lucky/module/weblog"
"github.com/gdy666/lucky/thirdlib/gdylib/ginutils"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/gdy666/lucky/thirdlib/gdylib/logsbuffer"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
var ReverseProxyServerStore sync.Map
var ReverseProxyServerStoreMu sync.Mutex
var GetValidSSLCertficateList func() []tls.Certificate
var SafeCheck func(string, string) bool
type SubReverProxyRule struct {
Key string `json:"Key"`
initOnce sync.Once
Locations []string `json:"Locations"` //长度大于1时均衡负载
locationMutex *sync.Mutex `json:"-"`
locationsCount int `json:"-"`
locationIndex uint64 `json:"-"`
EnableAccessLog bool `json:"EnableAccessLog"` //开启日志
LogLevel int `json:"LogLevel"` //日志输出级别
LogOutputToConsole bool `json:"LogOutputToConsole"` //日志输出到终端
AccessLogMaxNum int `json:"AccessLogMaxNum"` //最大条数
WebListShowLastLogMaxCount int `json:"WebListShowLastLogMaxCount"` //前端列表显示最新日志最大条数
ForwardedByClientIP bool `json:"ForwardedByClientIP"`
TrustedCIDRsStrList []string `json:"TrustedCIDRsStrList"`
RemoteIPHeaders []string `json:"RemoteIPHeaders"` //识别客户端原始IP的Http请求头
TrustedProxyCIDRs []*net.IPNet `json:"-"`
AddRemoteIPToHeader bool `json:"AddRemoteIPToHeader"` //追加客户端连接IP到指定Header
AddRemoteIPHeaderKey string `json:"AddRemoteIPHeaderKey"`
EnableBasicAuth bool `json:"EnableBasicAuth"` //启用BasicAuth认证
BasicAuthUser string `json:"BasicAuthUser"` //如果配置此参数,暴露出去的 HTTP 服务需要采用 Basic Auth 的鉴权才能访问
BasicAuthPasswd string `json:"BasicAuthPasswd"` //结合 BasicAuthUser 使用
SafeIPMode string `json:"SafeIPMode"` //IP过滤模式 黑白名单
SafeUserAgentMode string `json:"SafeUserAgentMode"` //UserAgent 过滤模式 黑白名单
UserAgentfilter []string `json:"UserAgentfilter"` //UserAgent 过滤内容
CustomRobotTxt bool `json:"CustomRobotTxt"`
RobotTxt string `json:"RobotTxt"`
//------------------
logsBuffer *logsbuffer.LogsBuffer
logrus *logrus.Logger
logger *log.Logger
}
type ReverseProxyRule struct {
RuleName string `json:"RuleName"`
RuleKey string `json:"RuleKey"`
Enable bool `json:"Enable"`
ListenIP string `json:"ListenIP"`
ListenPort int `json:"ListenPort"`
EnableTLS bool `json:"EnableTLS"`
Network string `json:"Network"`
DefaultProxy struct {
SubReverProxyRule
} `json:"DefaultProxy"`
ProxyList []ReverseProxy `json:"ProxyList"`
domainsMap *sync.Map
initOnec sync.Once
}
func (r *ReverseProxyRule) Init() {
r.initOnec.Do(func() {
r.initDomainsMap()
})
}
func (r *SubReverProxyRule) Logf(level logrus.Level, c *gin.Context, format string, v ...any) {
clientIP := r.ClientIP(c)
remoteIP := c.RemoteIP()
method := c.Request.Method
host := c.Request.Host
//hostname, hostport := httputils.SplitHostPort(c.Request.Host)
url := c.Request.URL.String()
//path := c.Request.URL.Path
r.GetLogrus().WithFields(logrus.Fields{
"ClientIP": clientIP,
"RemoteIP": remoteIP,
"Method": method,
"Host": host,
// "Hostname": hostname,
// "Hostport": hostport,
"URL": url,
//"path": path,
"UserAgent": c.Request.UserAgent(),
}).Logf(level, format, v...)
}
func (r *SubReverProxyRule) HandlerReverseProxy(remote *url.URL, host, path string, c *gin.Context) {
proxy := httputil.NewSingleHostReverseProxy(remote)
proxy.Director = func(req *http.Request) {
req.Header = c.Request.Header
req.Host = host //remote.Host
//req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
req.URL.Path = path
if r.AddRemoteIPToHeader && r.AddRemoteIPHeaderKey != "" {
cip := r.ClientIP(c)
req.Header.Add(r.AddRemoteIPHeaderKey, cip)
}
}
proxy.ErrorLog = r.GetLogger()
proxy.ServeHTTP(c.Writer, c.Request)
}
func (r *SubReverProxyRule) Fire(entry *logrus.Entry) error {
if !r.LogOutputToConsole {
return nil
}
s, _ := entry.String()
log.Print(s)
return nil
}
func (r *SubReverProxyRule) Levels() []logrus.Level {
return logrus.AllLevels
}
func (r *SubReverProxyRule) GetLogrus() *logrus.Logger {
if r.logrus == nil {
r.logrus = logrus.New()
r.logrus.SetLevel(logrus.Level(r.LogLevel))
r.logrus.SetOutput(r.GetLogsBuffer())
r.logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
DisableTimestamp: true,
DisableHTMLEscape: true,
DataKey: "ExtInfo",
})
r.logrus.AddHook(r)
}
return r.logrus
}
func (r *SubReverProxyRule) GetLogger() *log.Logger {
if r.logger == nil {
r.logger = log.New(r.GetLogsBuffer(), "", log.LstdFlags)
}
return r.logger
}
func (r *SubReverProxyRule) GetLogsBuffer() *logsbuffer.LogsBuffer {
if r.logsBuffer == nil {
r.logsBuffer = logsbuffer.CreateLogbuffer("reverseproxy:"+r.Key, r.AccessLogMaxNum)
}
return r.logsBuffer
}
func (r *SubReverProxyRule) checkupClientIP(ip string) bool {
return SafeCheck(r.SafeIPMode, ip)
}
func (r *SubReverProxyRule) checkupUserAgent(ua string) bool {
isContains := false
for _, c := range r.UserAgentfilter {
if strings.Contains(ua, c) {
isContains = true
break
}
}
switch r.SafeUserAgentMode {
case "whitelist":
return isContains
case "blacklist":
return !isContains
default:
return false
}
}
func (r *ReverseProxyRule) ReverseProxyHandler(c *gin.Context) {
path := c.Param("proxyPath")
hostName, _ := httputils.SplitHostPort(c.Request.Host)
rule, ok := r.GetSubRuleByDomain(hostName)
var subRule *SubReverProxyRule = nil
if ok && rule.Enable {
subRule = &rule.SubReverProxyRule
} else {
subRule = &r.DefaultProxy.SubReverProxyRule
}
if !subRule.checkupClientIP(subRule.ClientIP(c)) { //IP检查
subRule.Logf(logrus.WarnLevel, c, "IP[%s]禁止访问,当前Ip检查模式[%s]", subRule.ClientIP(c), subRule.SafeIPMode)
c.Abort()
return
}
if !subRule.checkupUserAgent(c.Request.UserAgent()) {
subRule.Logf(logrus.WarnLevel, c, "IP[%s]UA[%s]禁止访问,当前UA检查模式[%s]", subRule.ClientIP(c), c.Request.UserAgent(), subRule.SafeUserAgentMode)
c.Abort()
return
}
if !subRule.BasicAuthHandler(c) {
subRule.Logf(logrus.WarnLevel, c, "BasicAuth认证不通过")
c.Abort()
return
}
if subRule.CustomRobotTxt && c.Request.RequestURI == "/robots.txt" {
if c.Request.Method != "GET" && c.Request.Method != "HEAD" {
status := http.StatusOK
if c.Request.Method != "OPTIONS" {
status = http.StatusMethodNotAllowed
}
c.Header("Allow", "GET,HEAD,OPTIONS")
c.AbortWithStatus(status)
return
}
c.Data(http.StatusOK, "text/plain", []byte(subRule.RobotTxt))
subRule.Logf(logrus.InfoLevel, c, "触发自定义robots.txt")
return
}
location := subRule.GetLocation()
if location == "" && subRule.Key == r.RuleKey {
subRule.Logf(logrus.InfoLevel, c, "域名[%s]没有对应后端地址,默认后端地址没有设置", hostName)
c.Abort()
return
}
if subRule.Key == r.RuleKey {
subRule.Logf(logrus.InfoLevel, c, "[%s] 指向默认后端地址[%s%s]", hostName, location, c.Request.URL.String())
} else {
subRule.Logf(logrus.InfoLevel, c, "[%s] 指向后端地址[%s%s]", hostName, location, c.Request.URL.String())
}
remote, err := url.Parse(location)
if err != nil {
subRule.Logf(logrus.ErrorLevel, c, "后端地址转换出错:%s", err.Error())
c.JSON(http.StatusBadGateway, gin.H{"ret": 1, "msg": fmt.Sprintf("后端地址[%s] 转换出错:%s", location, err.Error())})
return
}
subRule.HandlerReverseProxy(remote, hostName, path, c)
}
func (r *ReverseProxyRule) GetSubRuleByDomain(domain string) (*ReverseProxy, bool) {
val, ok := r.domainsMap.Load(domain)
if !ok {
return nil, false
}
return val.(*ReverseProxy), true
}
type ReverseProxy struct {
SubReverProxyRule
Enable bool `json:"Enable"`
Remark string `json:"Remark"`
Domains []string `json:"Domains"` //自定义域名
}
func (r *ReverseProxyRule) GetServer() *http.Server {
s, loaded := ReverseProxyServerStore.Load(r.RuleKey)
if !loaded {
return nil
}
return s.(*http.Server)
}
func (r *ReverseProxyRule) SetServer(s *http.Server) {
if s == nil {
ReverseProxyServerStore.Delete(r.RuleKey)
return
}
ReverseProxyServerStore.Store(r.RuleKey, s)
}
func (r *ReverseProxyRule) ServerStart() error {
// r.smu.Lock()
// defer r.smu.Unlock()
ReverseProxyServerStoreMu.Lock()
defer ReverseProxyServerStoreMu.Unlock()
server := r.GetServer()
if server != nil {
return fmt.Errorf("RuleServer[%s]已经启动,请勿重复启动", r.Addr())
}
ginR := gin.New()
ginR.Any("/*proxyPath", r.ReverseProxyHandler)
server = &http.Server{
Addr: r.Addr(),
Handler: ginR,
ErrorLog: r.DefaultProxy.GetLogger(),
}
//***************************
var err error
server.TLSConfig = &tls.Config{}
if r.EnableTLS {
certList := GetValidSSLCertficateList() //ssl.GetValidSSLCertficateList()
server.TLSConfig.Certificates = certList
}
//server.TLSConfig.Certificates = make([]tls.Certificate, 3)
//****************************
ln, err := net.Listen(r.Network, r.Addr())
if err != nil {
return err
}
var serveResult error
go func() {
if !r.EnableTLS {
serveResult = server.Serve(ln)
return
}
if len(server.TLSConfig.Certificates) <= 0 {
log.Printf("可用证书列表为空,%s 未能启用TLS", r.Addr())
serveResult = server.Serve(ln)
return
}
log.Printf("%s 已启用TLS", r.Addr())
serveResult = server.ServeTLS(ln, "", "")
}()
<-time.After(time.Millisecond * 300)
defer func() {
if serveResult == nil {
r.SetServer(server)
}
}()
return serveResult
}
func (r *ReverseProxyRule) ServerStop() {
ReverseProxyServerStoreMu.Lock()
defer ReverseProxyServerStoreMu.Unlock()
server := r.GetServer()
if server == nil {
return
}
server.Close()
r.SetServer(nil)
}
func (r *ReverseProxyRule) initDomainsMap() error {
r.domainsMap = &sync.Map{}
for i := range r.ProxyList {
for j := range r.ProxyList[i].Domains {
_, loaded := r.domainsMap.LoadOrStore(r.ProxyList[i].Domains[j], &r.ProxyList[i])
if loaded {
return fmt.Errorf("前端域名[%s]冲突", r.ProxyList[i].Domains[j])
}
}
}
return nil
}
func (r *SubReverProxyRule) initOnceExec() {
r.initOnce.Do(func() {
r.locationsCount = len(r.Locations)
r.InitTrustedProxyCIDRs()
r.locationMutex = &sync.Mutex{}
})
}
func (r *SubReverProxyRule) GetLocation() string {
r.initOnceExec()
r.locationMutex.Lock()
defer func() {
r.locationIndex++
r.locationMutex.Unlock()
}()
if r.locationsCount == 0 {
return ""
}
return r.Locations[r.locationIndex%uint64(r.locationsCount)]
}
func (r *SubReverProxyRule) BasicAuthHandler(c *gin.Context) bool {
if !r.EnableBasicAuth || r.BasicAuthUser == "" {
return true
}
realm := "Basic realm=" + strconv.Quote("Authorization Required")
pairs := ginutils.ProcessAccounts(gin.Accounts{r.BasicAuthUser: r.BasicAuthPasswd})
user, found := pairs.SearchCredential(c.GetHeader("Authorization"))
if !found {
// Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm)
c.AbortWithStatus(http.StatusUnauthorized)
return false
}
c.Set("user", user)
return true
}
func (r *SubReverProxyRule) InitTrustedProxyCIDRs() error {
var res []*net.IPNet
for i := range r.TrustedCIDRsStrList {
if strings.TrimSpace(r.TrustedCIDRsStrList[i]) == "" {
continue
}
_, cidr, err := net.ParseCIDR(r.TrustedCIDRsStrList[i])
if err != nil {
return fmt.Errorf("[%s]网段格式有误", r.TrustedCIDRsStrList[i])
}
res = append(res, cidr)
}
r.TrustedProxyCIDRs = res
return nil
}
func (r *SubReverProxyRule) ClientIP(c *gin.Context) string {
remoteIP := net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
}
trusted := r.isTrustedProxy(remoteIP)
if trusted && r.ForwardedByClientIP && r.RemoteIPHeaders != nil {
for _, headerName := range r.RemoteIPHeaders {
ip, valid := r.validateHeader(c.Request.Header.Get(headerName))
if valid {
return ip
}
}
}
return remoteIP.String()
}
func (r *SubReverProxyRule) validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
if ip == nil {
break
}
if (i == 0) || (!r.isTrustedProxy(ip)) {
return ipStr, true
}
}
return "", false
}
func (r *SubReverProxyRule) isTrustedProxy(ip net.IP) bool {
r.initOnceExec()
if r.TrustedProxyCIDRs == nil {
return false
}
for _, cidr := range r.TrustedProxyCIDRs {
if cidr.Contains(ip) {
return true
}
}
return false
}
func (r *ReverseProxyRule) Addr() string {
return fmt.Sprintf("%s:%d", r.ListenIP, r.ListenPort)
}
func (r *ReverseProxyRule) GetLastLogs() map[string][]any {
res := make(map[string][]any)
res["default"] = r.DefaultProxy.GetLogsBuffer().GetLastLogs(weblog.WebLogConvert, r.DefaultProxy.WebListShowLastLogMaxCount)
for i := range r.ProxyList {
res[r.ProxyList[i].Key] = r.ProxyList[i].GetLogsBuffer().GetLastLogs(
weblog.WebLogConvert, r.ProxyList[i].WebListShowLastLogMaxCount)
}
return res
}