refactor: project directory structure

This commit is contained in:
0xJacky 2023-11-26 18:59:12 +08:00
parent c1193a5b8c
commit e5a5889931
No known key found for this signature in database
GPG key ID: B6E4A6E4A561BAF0
367 changed files with 710 additions and 756 deletions

126
router/middleware.go Normal file
View 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
View 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
View 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
View 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
View 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
}