mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
refactor: project directory structure
This commit is contained in:
parent
c1193a5b8c
commit
e5a5889931
367 changed files with 710 additions and 756 deletions
126
router/middleware.go
Normal file
126
router/middleware.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/0xJacky/Nginx-UI/app"
|
||||
"github.com/0xJacky/Nginx-UI/internal/logger"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cast"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func recovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
errorAction := "panic"
|
||||
if action, ok := c.Get("maybe_error"); ok {
|
||||
errorActionMsg := cast.ToString(action)
|
||||
if errorActionMsg != "" {
|
||||
errorAction = errorActionMsg
|
||||
}
|
||||
}
|
||||
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(),
|
||||
"error": errorAction,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if model.CheckToken(token) < 1 {
|
||||
abortWithAuthFailure()
|
||||
return
|
||||
}
|
||||
|
||||
if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" {
|
||||
c.Set("ProxyNodeID", nodeID)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
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(frontend.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)
|
||||
}
|
||||
}
|
||||
}
|
154
router/operation_sync.go
Normal file
154
router/operation_sync.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
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
|
||||
}
|
92
router/proxy.go
Normal file
92
router/proxy.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
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
|
||||
}
|
||||
|
||||
u, err := url.JoinPath(environment.URL, c.Request.RequestURI)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
decodedUri, err := url.QueryUnescape(u)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Proxy request", decodedUri)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(c.Request.Method, decodedUri, c.Request.Body)
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
56
router/proxy_ws.go
Normal file
56
router/proxy_ws.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
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)
|
||||
}
|
||||
}
|
157
router/routers.go
Normal file
157
router/routers.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
api2 "github.com/0xJacky/Nginx-UI/api"
|
||||
"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(OperationSync())
|
||||
|
||||
r.Use(static.Serve("/", mustFS("")))
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"message": "not found",
|
||||
})
|
||||
})
|
||||
|
||||
root := r.Group("/api")
|
||||
{
|
||||
root.GET("install", api2.InstallLockCheck)
|
||||
root.POST("install", api2.InstallNginxUI)
|
||||
|
||||
root.POST("/login", api2.Login)
|
||||
root.DELETE("/logout", api2.Logout)
|
||||
|
||||
root.GET("/casdoor_uri", api2.GetCasdoorUri)
|
||||
root.POST("/casdoor_callback", api2.CasdoorCallback)
|
||||
|
||||
// translation
|
||||
root.GET("translation/:code", api2.GetTranslation)
|
||||
|
||||
w := root.Group("/", authRequired(), proxyWs())
|
||||
{
|
||||
// Analytic
|
||||
w.GET("analytic", api2.Analytic)
|
||||
w.GET("analytic/intro", api2.GetNodeStat)
|
||||
w.GET("analytic/nodes", api2.GetNodesAnalytic)
|
||||
// pty
|
||||
w.GET("pty", api2.Pty)
|
||||
// Nginx log
|
||||
w.GET("nginx_log", api2.NginxLog)
|
||||
}
|
||||
|
||||
g := root.Group("/", authRequired(), proxy())
|
||||
{
|
||||
|
||||
g.GET("analytic/init", api2.GetAnalyticInit)
|
||||
|
||||
g.GET("users", api2.GetUsers)
|
||||
g.GET("user/:id", api2.GetUser)
|
||||
g.POST("user", api2.AddUser)
|
||||
g.POST("user/:id", api2.EditUser)
|
||||
g.DELETE("user/:id", api2.DeleteUser)
|
||||
|
||||
g.GET("domains", api2.GetDomains)
|
||||
g.GET("domain/:name", api2.GetDomain)
|
||||
|
||||
// Modify site configuration directly
|
||||
g.POST("domain/:name", api2.SaveDomain)
|
||||
|
||||
// Transform NgxConf to nginx configuration
|
||||
g.POST("ngx/build_config", api2.BuildNginxConfig)
|
||||
// Tokenized nginx configuration to NgxConf
|
||||
g.POST("ngx/tokenize_config", api2.TokenizeNginxConfig)
|
||||
// Format nginx configuration code
|
||||
g.POST("ngx/format_code", api2.FormatNginxConfig)
|
||||
|
||||
g.POST("nginx/reload", api2.ReloadNginx)
|
||||
g.POST("nginx/restart", api2.RestartNginx)
|
||||
g.POST("nginx/test", api2.TestNginx)
|
||||
g.GET("nginx/status", api2.NginxStatus)
|
||||
|
||||
g.POST("domain/:name/enable", api2.EnableDomain)
|
||||
g.POST("domain/:name/disable", api2.DisableDomain)
|
||||
g.POST("domain/:name/advance", api2.DomainEditByAdvancedMode)
|
||||
|
||||
g.DELETE("domain/:name", api2.DeleteDomain)
|
||||
|
||||
g.POST("domain/:name/duplicate", api2.DuplicateSite)
|
||||
g.GET("domain/:name/cert", api2.IssueCert)
|
||||
|
||||
g.GET("configs", api2.GetConfigs)
|
||||
g.GET("config/*name", api2.GetConfig)
|
||||
g.POST("config", api2.AddConfig)
|
||||
g.POST("config/*name", api2.EditConfig)
|
||||
|
||||
//g.GET("backups", api.GetFileBackupList)
|
||||
//g.GET("backup/:id", api.GetFileBackup)
|
||||
|
||||
g.GET("template", api2.GetTemplate)
|
||||
g.GET("template/configs", api2.GetTemplateConfList)
|
||||
g.GET("template/blocks", api2.GetTemplateBlockList)
|
||||
g.GET("template/block/:name", api2.GetTemplateBlock)
|
||||
g.POST("template/block/:name", api2.GetTemplateBlock)
|
||||
|
||||
g.GET("certs", api2.GetCertList)
|
||||
g.GET("cert/:id", api2.GetCert)
|
||||
g.POST("cert", api2.AddCert)
|
||||
g.POST("cert/:id", api2.ModifyCert)
|
||||
g.DELETE("cert/:id", api2.RemoveCert)
|
||||
|
||||
// Add domain to auto-renew cert list
|
||||
g.POST("auto_cert/:name", api2.AddDomainToAutoCert)
|
||||
// Delete domain from auto-renew cert list
|
||||
g.DELETE("auto_cert/:name", api2.RemoveDomainFromAutoCert)
|
||||
g.GET("auto_cert/dns/providers", api2.GetDNSProvidersList)
|
||||
g.GET("auto_cert/dns/provider/:code", api2.GetDNSProvider)
|
||||
|
||||
// DNS Credential
|
||||
g.GET("dns_credentials", api2.GetDnsCredentialList)
|
||||
g.GET("dns_credential/:id", api2.GetDnsCredential)
|
||||
g.POST("dns_credential", api2.AddDnsCredential)
|
||||
g.POST("dns_credential/:id", api2.EditDnsCredential)
|
||||
g.DELETE("dns_credential/:id", api2.DeleteDnsCredential)
|
||||
|
||||
g.POST("nginx_log", api2.GetNginxLogPage)
|
||||
|
||||
// Settings
|
||||
g.GET("settings", api2.GetSettings)
|
||||
g.POST("settings", api2.SaveSettings)
|
||||
|
||||
// Upgrade
|
||||
g.GET("upgrade/release", api2.GetRelease)
|
||||
g.GET("upgrade/current", api2.GetCurrentVersion)
|
||||
g.GET("upgrade/perform", api2.PerformCoreUpgrade)
|
||||
|
||||
// ChatGPT
|
||||
g.POST("chat_gpt", api2.MakeChatCompletionRequest)
|
||||
g.POST("chat_gpt_record", api2.StoreChatGPTRecord)
|
||||
|
||||
// Environment
|
||||
g.GET("environments", api2.GetEnvironmentList)
|
||||
envGroup := g.Group("environment")
|
||||
{
|
||||
envGroup.GET("/:id", api2.GetEnvironment)
|
||||
envGroup.POST("", api2.AddEnvironment)
|
||||
envGroup.POST("/:id", api2.EditEnvironment)
|
||||
envGroup.DELETE("/:id", api2.DeleteEnvironment)
|
||||
}
|
||||
|
||||
// node
|
||||
g.GET("node", api2.GetCurrentNode)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue