feat: deploy config to remote nodes #359

This commit is contained in:
Jacky 2024-07-26 13:53:38 +08:00
parent e75dce92ad
commit 1c1da92363
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
46 changed files with 1480 additions and 605 deletions

View file

@ -1,25 +0,0 @@
package router
import (
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"net/http"
)
func ipWhiteList() gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "127.0.0.1" {
c.Next()
return
}
if !lo.Contains(settings.AuthSettings.IPWhiteList, clientIP) {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
}

View file

@ -1,156 +0,0 @@
package router
import (
"encoding/base64"
"github.com/0xJacky/Nginx-UI/app"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/user"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"io/fs"
"net/http"
"path"
"runtime"
"strings"
)
func recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, 1024)
runtime.Stack(buf, false)
logger.Errorf("%s\n%s", err, buf)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": err.(error).Error(),
})
}
}()
c.Next()
}
}
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
abortWithAuthFailure := func() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"message": "Authorization failed",
})
}
token := c.GetHeader("Authorization")
if token == "" {
if token = c.GetHeader("X-Node-Secret"); token != "" && token == settings.ServerSettings.NodeSecret {
c.Set("NodeSecret", token)
c.Next()
return
} else {
c.Set("ProxyNodeID", c.Query("x_node_id"))
tokenBytes, _ := base64.StdEncoding.DecodeString(c.Query("token"))
token = string(tokenBytes)
if token == "" {
abortWithAuthFailure()
return
}
}
}
u, ok := user.GetTokenUser(token)
if !ok {
abortWithAuthFailure()
return
}
c.Set("user", u)
if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" {
c.Set("ProxyNodeID", nodeID)
}
c.Next()
}
}
func required2FA() gin.HandlerFunc {
return func(c *gin.Context) {
u, ok := c.Get("user")
if !ok {
c.Next()
return
}
cUser := u.(*model.Auth)
if !cUser.EnabledOTP() {
c.Next()
return
}
ssid := c.GetHeader("X-Secure-Session-ID")
if ssid == "" {
ssid = c.Query("X-Secure-Session-ID")
}
if ssid == "" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"message": "Secure Session ID is empty",
})
return
}
if user.VerifySecureSessionID(ssid, cUser.ID) {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"message": "Secure Session ID is invalid",
})
return
}
}
type serverFileSystemType struct {
http.FileSystem
}
func (f serverFileSystemType) Exists(prefix string, _path string) bool {
file, err := f.Open(path.Join(prefix, _path))
if file != nil {
defer func(file http.File) {
err = file.Close()
if err != nil {
logger.Error("file not found", err)
}
}(file)
}
return err == nil
}
func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
sub, err := fs.Sub(app.DistFS, path.Join("dist", dir))
if err != nil {
logger.Error(err)
return
}
serverFileSystem = serverFileSystemType{
http.FS(sub),
}
return
}
func cacheJs() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.String(), "js") {
c.Header("Cache-Control", "max-age: 1296000")
if c.Request.Header.Get("If-Modified-Since") == settings.LastModified {
c.AbortWithStatus(http.StatusNotModified)
}
c.Header("Last-Modified", settings.LastModified)
}
}
}

View file

@ -1,154 +0,0 @@
package router
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"github.com/0xJacky/Nginx-UI/internal/analytic"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"io"
"net/http"
"net/url"
"regexp"
"sync"
)
type ErrorRes struct {
Message string `json:"message"`
}
type toolBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r toolBodyWriter) Write(b []byte) (int, error) {
return r.body.Write(b)
}
// OperationSync 针对配置了vip的环境操作进行同步
func OperationSync() gin.HandlerFunc {
return func(c *gin.Context) {
bodyBytes, _ := PeekRequest(c.Request)
wb := &toolBodyWriter{
body: &bytes.Buffer{},
ResponseWriter: c.Writer,
}
c.Writer = wb
c.Next()
if c.Request.Method == "GET" || !statusValid(c.Writer.Status()) { // 请求有问题,无需执行同步操作
return
}
totalCount := 0
successCount := 0
detailMsg := ""
// 后置处理操作同步
wg := sync.WaitGroup{}
for _, node := range analytic.NodeMap {
wg.Add(1)
node := node
go func(data analytic.Node) {
defer wg.Done()
if node.OperationSync && node.Status && requestUrlMatch(c.Request.URL.Path, data) { // 开启操作同步且当前状态正常
totalCount++
if err := syncNodeOperation(c, data, bodyBytes); err != nil {
detailMsg += fmt.Sprintf("node_name: %s, err_msg: %s; ", data.Name, err)
return
}
successCount++
}
}(*node)
}
wg.Wait()
if successCount < totalCount { // 如果有错误,替换原来的消息内容
originBytes := wb.body
logger.Infof("origin response body: %s", originBytes)
// clear Origin Buffer
wb.body = &bytes.Buffer{}
wb.ResponseWriter.WriteHeader(http.StatusInternalServerError)
errorRes := ErrorRes{
Message: fmt.Sprintf("operation sync failed, total: %d, success: %d, fail: %d, detail: %s", totalCount, successCount, totalCount-successCount, detailMsg),
}
byts, _ := json.Marshal(errorRes)
_, err := wb.Write(byts)
if err != nil {
logger.Error(err)
}
}
_, err := wb.ResponseWriter.Write(wb.body.Bytes())
if err != nil {
logger.Error(err)
}
}
}
func PeekRequest(request *http.Request) ([]byte, error) {
if request.Body != nil {
byts, err := io.ReadAll(request.Body) // io.ReadAll as Go 1.16, below please use ioutil.ReadAll
if err != nil {
return nil, err
}
request.Body = io.NopCloser(bytes.NewReader(byts))
return byts, nil
}
return make([]byte, 0), nil
}
func requestUrlMatch(url string, node analytic.Node) bool {
p, _ := regexp.Compile(node.SyncApiRegex)
result := p.FindAllString(url, -1)
if len(result) > 0 && result[0] == url {
return true
}
return false
}
func statusValid(code int) bool {
return code < http.StatusMultipleChoices
}
func syncNodeOperation(c *gin.Context, node analytic.Node, bodyBytes []byte) error {
u, err := url.JoinPath(node.URL, c.Request.RequestURI)
if err != nil {
return err
}
decodedUri, err := url.QueryUnescape(u)
if err != nil {
return err
}
logger.Debugf("syncNodeOperation request: %s, node_id: %d, node_name: %s", decodedUri, node.ID, node.Name)
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest(c.Request.Method, decodedUri, bytes.NewReader(bodyBytes))
req.Header.Set("X-Node-Secret", node.Token)
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
byts, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if !statusValid(res.StatusCode) {
errRes := ErrorRes{}
if err = json.Unmarshal(byts, &errRes); err != nil {
return err
}
return errors.New(errRes.Message)
}
logger.Debug("syncNodeOperation result: ", string(byts))
return nil
}

View file

@ -1,102 +0,0 @@
package router
import (
"crypto/tls"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"io"
"net/http"
"net/url"
)
func proxy() gin.HandlerFunc {
return func(c *gin.Context) {
nodeID, ok := c.Get("ProxyNodeID")
if !ok {
c.Next()
return
}
id := cast.ToInt(nodeID)
if id == 0 {
c.Next()
return
}
defer c.Abort()
env := query.Environment
environment, err := env.Where(env.ID.Eq(id)).First()
if err != nil {
logger.Error(err)
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
"message": err.Error(),
})
return
}
baseUrl, err := url.Parse(environment.URL)
if err != nil {
logger.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
proxyUrl, err := baseUrl.Parse(c.Request.RequestURI)
if err != nil {
logger.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
logger.Debug("Proxy request", proxyUrl.String())
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest(c.Request.Method, proxyUrl.String(), c.Request.Body)
if err != nil {
logger.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
req.Header.Set("X-Node-Secret", environment.Token)
resp, err := client.Do(req)
if err != nil {
logger.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}
defer resp.Body.Close()
// rewrite status code to fix https://github.com/0xJacky/nginx-ui/issues/342
if resp.StatusCode == http.StatusForbidden {
resp.StatusCode = http.StatusServiceUnavailable
}
c.Writer.WriteHeader(resp.StatusCode)
c.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
logger.Error(err)
return
}
}
}

View file

@ -1,56 +0,0 @@
package router
import (
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/pretty66/websocketproxy"
"github.com/spf13/cast"
"net/http"
)
func proxyWs() gin.HandlerFunc {
return func(c *gin.Context) {
nodeID, ok := c.Get("ProxyNodeID")
if !ok {
c.Next()
return
}
id := cast.ToInt(nodeID)
if id == 0 {
c.Next()
return
}
defer c.Abort()
env := query.Environment
environment, err := env.Where(env.ID.Eq(id)).First()
if err != nil {
logger.Error(err)
return
}
decodedUri, err := environment.GetWebSocketURL(c.Request.RequestURI)
if err != nil {
logger.Error(err)
return
}
logger.Debug("Proxy request", decodedUri)
wp, err := websocketproxy.NewProxy(decodedUri, func(r *http.Request) error {
r.Header.Set("X-Node-Secret", environment.Token)
return nil
})
if err != nil {
logger.Error(err)
return
}
wp.Proxy(c.Writer, c.Request)
}
}

View file

@ -1,83 +1,83 @@
package router
import (
"github.com/0xJacky/Nginx-UI/api/analytic"
"github.com/0xJacky/Nginx-UI/api/certificate"
"github.com/0xJacky/Nginx-UI/api/cluster"
"github.com/0xJacky/Nginx-UI/api/config"
"github.com/0xJacky/Nginx-UI/api/nginx"
"github.com/0xJacky/Nginx-UI/api/notification"
"github.com/0xJacky/Nginx-UI/api/openai"
"github.com/0xJacky/Nginx-UI/api/settings"
"github.com/0xJacky/Nginx-UI/api/sites"
"github.com/0xJacky/Nginx-UI/api/streams"
"github.com/0xJacky/Nginx-UI/api/system"
"github.com/0xJacky/Nginx-UI/api/template"
"github.com/0xJacky/Nginx-UI/api/terminal"
"github.com/0xJacky/Nginx-UI/api/upstream"
"github.com/0xJacky/Nginx-UI/api/user"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"net/http"
"github.com/0xJacky/Nginx-UI/api/analytic"
"github.com/0xJacky/Nginx-UI/api/certificate"
"github.com/0xJacky/Nginx-UI/api/cluster"
"github.com/0xJacky/Nginx-UI/api/config"
"github.com/0xJacky/Nginx-UI/api/nginx"
"github.com/0xJacky/Nginx-UI/api/notification"
"github.com/0xJacky/Nginx-UI/api/openai"
"github.com/0xJacky/Nginx-UI/api/settings"
"github.com/0xJacky/Nginx-UI/api/sites"
"github.com/0xJacky/Nginx-UI/api/streams"
"github.com/0xJacky/Nginx-UI/api/system"
"github.com/0xJacky/Nginx-UI/api/template"
"github.com/0xJacky/Nginx-UI/api/terminal"
"github.com/0xJacky/Nginx-UI/api/upstream"
"github.com/0xJacky/Nginx-UI/api/user"
"github.com/0xJacky/Nginx-UI/internal/middleware"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"net/http"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(recovery())
r.Use(cacheJs())
r.Use(ipWhiteList())
r := gin.New()
r.Use(
gin.Logger(),
middleware.Recovery(),
middleware.CacheJs(),
middleware.IPWhiteList(),
static.Serve("/", middleware.MustFs("")),
)
//r.Use(OperationSync())
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"message": "not found",
})
})
r.Use(static.Serve("/", mustFS("")))
root := r.Group("/api")
{
system.InitPublicRouter(root)
user.InitAuthRouter(root)
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"message": "not found",
})
})
// Authorization required not websocket request
g := root.Group("/", middleware.AuthRequired(), middleware.Proxy())
{
user.InitUserRouter(g)
analytic.InitRouter(g)
user.InitManageUserRouter(g)
nginx.InitRouter(g)
sites.InitRouter(g)
streams.InitRouter(g)
config.InitRouter(g)
template.InitRouter(g)
certificate.InitCertificateRouter(g)
certificate.InitDNSCredentialRouter(g)
certificate.InitAcmeUserRouter(g)
system.InitPrivateRouter(g)
settings.InitRouter(g)
openai.InitRouter(g)
cluster.InitRouter(g)
notification.InitRouter(g)
}
root := r.Group("/api")
{
system.InitPublicRouter(root)
user.InitAuthRouter(root)
// Authorization required and websocket request
w := root.Group("/", middleware.AuthRequired(), middleware.ProxyWs())
{
analytic.InitWebSocketRouter(w)
certificate.InitCertificateWebSocketRouter(w)
o := w.Group("", middleware.RequireSecureSession())
{
terminal.InitRouter(o)
}
nginx.InitNginxLogRouter(w)
upstream.InitRouter(w)
system.InitWebSocketRouter(w)
}
}
// Authorization required not websocket request
g := root.Group("/", authRequired(), proxy())
{
user.InitUserRouter(g)
analytic.InitRouter(g)
user.InitManageUserRouter(g)
nginx.InitRouter(g)
sites.InitRouter(g)
streams.InitRouter(g)
config.InitRouter(g)
template.InitRouter(g)
certificate.InitCertificateRouter(g)
certificate.InitDNSCredentialRouter(g)
certificate.InitAcmeUserRouter(g)
system.InitPrivateRouter(g)
settings.InitRouter(g)
openai.InitRouter(g)
cluster.InitRouter(g)
notification.InitRouter(g)
}
// Authorization required and websocket request
w := root.Group("/", authRequired(), proxyWs())
{
analytic.InitWebSocketRouter(w)
certificate.InitCertificateWebSocketRouter(w)
o := w.Group("", required2FA())
{
terminal.InitRouter(o)
}
nginx.InitNginxLogRouter(w)
upstream.InitRouter(w)
system.InitWebSocketRouter(w)
}
}
return r
return r
}