2019-12-08 03:59:58 -05:00
|
|
|
package controller
|
|
|
|
|
|
|
|
import (
|
2024-10-21 02:30:50 -04:00
|
|
|
"errors"
|
2019-12-08 03:59:58 -05:00
|
|
|
"fmt"
|
2024-11-29 08:31:39 -05:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
2023-11-28 20:42:51 -05:00
|
|
|
"log"
|
2021-07-14 11:53:37 -04:00
|
|
|
"net/http"
|
2024-10-24 09:33:36 -04:00
|
|
|
"os"
|
2024-11-29 11:02:45 -05:00
|
|
|
"path"
|
2024-12-21 11:05:41 -05:00
|
|
|
"slices"
|
2024-10-19 23:47:45 -04:00
|
|
|
"strings"
|
2019-12-08 03:59:58 -05:00
|
|
|
|
2024-10-19 12:09:16 -04:00
|
|
|
jwt "github.com/appleboy/gin-jwt/v2"
|
2021-05-10 06:04:38 -04:00
|
|
|
"github.com/gin-contrib/pprof"
|
2019-12-08 03:59:58 -05:00
|
|
|
"github.com/gin-gonic/gin"
|
2024-10-19 11:14:53 -04:00
|
|
|
swaggerfiles "github.com/swaggo/files"
|
|
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
2019-12-08 03:59:58 -05:00
|
|
|
|
2024-11-28 06:38:54 -05:00
|
|
|
"github.com/nezhahq/nezha/cmd/dashboard/controller/waf"
|
|
|
|
docs "github.com/nezhahq/nezha/cmd/dashboard/docs"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
|
|
"github.com/nezhahq/nezha/service/singleton"
|
2019-12-08 03:59:58 -05:00
|
|
|
)
|
|
|
|
|
2024-12-06 12:18:34 -05:00
|
|
|
func ServeWeb(frontendDist fs.FS) http.Handler {
|
2019-12-08 10:18:29 -05:00
|
|
|
gin.SetMode(gin.ReleaseMode)
|
2021-05-10 06:04:38 -04:00
|
|
|
r := gin.Default()
|
2024-10-24 09:33:36 -04:00
|
|
|
|
2024-07-14 07:41:50 -04:00
|
|
|
if singleton.Conf.Debug {
|
|
|
|
gin.SetMode(gin.DebugMode)
|
|
|
|
pprof.Register(r)
|
|
|
|
}
|
2024-10-19 23:47:45 -04:00
|
|
|
if singleton.Conf.Debug {
|
2024-10-24 12:19:44 -04:00
|
|
|
log.Printf("NEZHA>> Swagger(%s) UI available at http://localhost:%d/swagger/index.html", docs.SwaggerInfo.Version, singleton.Conf.ListenPort)
|
2024-10-19 23:47:45 -04:00
|
|
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
|
|
|
}
|
2024-10-20 02:05:43 -04:00
|
|
|
|
2024-11-22 10:57:25 -05:00
|
|
|
r.Use(waf.RealIp)
|
|
|
|
r.Use(waf.Waf)
|
2024-10-19 23:47:45 -04:00
|
|
|
r.Use(recordPath)
|
2024-11-29 08:31:39 -05:00
|
|
|
|
2024-12-06 12:18:34 -05:00
|
|
|
routers(r, frontendDist)
|
2021-08-10 08:13:17 -04:00
|
|
|
|
2024-10-22 11:44:50 -04:00
|
|
|
return r
|
2019-12-08 03:59:58 -05:00
|
|
|
}
|
|
|
|
|
2024-12-06 12:18:34 -05:00
|
|
|
func routers(r *gin.Engine, frontendDist fs.FS) {
|
2024-10-19 12:09:16 -04:00
|
|
|
authMiddleware, err := jwt.New(initParams())
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("JWT Error:" + err.Error())
|
|
|
|
}
|
2024-10-20 11:23:04 -04:00
|
|
|
if err := authMiddleware.MiddlewareInit(); err != nil {
|
|
|
|
log.Fatal("authMiddleware.MiddlewareInit Error:" + err.Error())
|
|
|
|
}
|
2024-10-20 02:05:43 -04:00
|
|
|
api := r.Group("api/v1")
|
|
|
|
api.POST("/login", authMiddleware.LoginHandler)
|
2024-10-19 12:09:16 -04:00
|
|
|
|
2024-10-21 02:30:50 -04:00
|
|
|
optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware))
|
2024-10-21 11:00:51 -04:00
|
|
|
optionalAuth.GET("/ws/server", commonHandler(serverStream))
|
|
|
|
optionalAuth.GET("/server-group", commonHandler(listServerGroup))
|
2024-10-20 11:23:04 -04:00
|
|
|
|
2024-12-05 08:00:02 -05:00
|
|
|
optionalAuth.GET("/service", commonHandler(showService))
|
2024-10-27 02:43:37 -04:00
|
|
|
optionalAuth.GET("/service/:id", commonHandler(listServiceHistory))
|
|
|
|
optionalAuth.GET("/service/server", commonHandler(listServerWithServices))
|
|
|
|
|
2024-10-27 01:10:07 -04:00
|
|
|
optionalAuth.GET("/setting", commonHandler(listConfig))
|
|
|
|
|
2024-10-20 02:05:43 -04:00
|
|
|
auth := api.Group("", authMiddleware.MiddlewareFunc())
|
2024-10-22 10:01:01 -04:00
|
|
|
|
2024-11-08 10:57:15 -05:00
|
|
|
auth.GET("/refresh-token", authMiddleware.RefreshHandler)
|
2024-10-21 11:00:51 -04:00
|
|
|
|
2024-10-22 10:01:01 -04:00
|
|
|
auth.POST("/terminal", commonHandler(createTerminal))
|
|
|
|
auth.GET("/ws/terminal/:id", commonHandler(terminalStream))
|
|
|
|
|
2024-10-24 21:09:08 -04:00
|
|
|
auth.GET("/file", commonHandler(createFM))
|
|
|
|
auth.GET("/ws/file/:id", commonHandler(fmStream))
|
|
|
|
|
2024-11-03 10:28:10 -05:00
|
|
|
auth.GET("/profile", commonHandler(getProfile))
|
2024-11-26 08:30:56 -05:00
|
|
|
auth.POST("/profile", commonHandler(updateProfile))
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/user", adminHandler(listUser))
|
|
|
|
auth.POST("/user", adminHandler(createUser))
|
|
|
|
auth.POST("/batch-delete/user", adminHandler(batchDeleteUser))
|
2024-10-22 09:19:30 -04:00
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/service/list", listHandler(listService))
|
2024-10-24 12:13:45 -04:00
|
|
|
auth.POST("/service", commonHandler(createService))
|
|
|
|
auth.PATCH("/service/:id", commonHandler(updateService))
|
|
|
|
auth.POST("/batch-delete/service", commonHandler(batchDeleteService))
|
2024-10-23 11:06:11 -04:00
|
|
|
|
2024-10-22 09:19:30 -04:00
|
|
|
auth.POST("/server-group", commonHandler(createServerGroup))
|
|
|
|
auth.PATCH("/server-group/:id", commonHandler(updateServerGroup))
|
2024-10-21 11:00:51 -04:00
|
|
|
auth.POST("/batch-delete/server-group", commonHandler(batchDeleteServerGroup))
|
|
|
|
|
2024-10-23 09:55:12 -04:00
|
|
|
auth.GET("/notification-group", commonHandler(listNotificationGroup))
|
|
|
|
auth.POST("/notification-group", commonHandler(createNotificationGroup))
|
|
|
|
auth.PATCH("/notification-group/:id", commonHandler(updateNotificationGroup))
|
|
|
|
auth.POST("/batch-delete/notification-group", commonHandler(batchDeleteNotificationGroup))
|
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/server", listHandler(listServer))
|
2024-10-22 09:19:30 -04:00
|
|
|
auth.PATCH("/server/:id", commonHandler(updateServer))
|
2024-10-21 11:00:51 -04:00
|
|
|
auth.POST("/batch-delete/server", commonHandler(batchDeleteServer))
|
2024-11-20 08:36:21 -05:00
|
|
|
auth.POST("/force-update/server", commonHandler(forceUpdateServer))
|
2024-10-21 11:00:51 -04:00
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/notification", listHandler(listNotification))
|
2024-10-23 09:55:12 -04:00
|
|
|
auth.POST("/notification", commonHandler(createNotification))
|
|
|
|
auth.PATCH("/notification/:id", commonHandler(updateNotification))
|
|
|
|
auth.POST("/batch-delete/notification", commonHandler(batchDeleteNotification))
|
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/alert-rule", listHandler(listAlertRule))
|
2024-10-25 20:16:57 -04:00
|
|
|
auth.POST("/alert-rule", commonHandler(createAlertRule))
|
|
|
|
auth.PATCH("/alert-rule/:id", commonHandler(updateAlertRule))
|
|
|
|
auth.POST("/batch-delete/alert-rule", commonHandler(batchDeleteAlertRule))
|
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/cron", listHandler(listCron))
|
2024-10-26 11:57:47 -04:00
|
|
|
auth.POST("/cron", commonHandler(createCron))
|
|
|
|
auth.PATCH("/cron/:id", commonHandler(updateCron))
|
|
|
|
auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron))
|
|
|
|
auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron))
|
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/ddns", listHandler(listDDNS))
|
2024-10-21 12:04:17 -04:00
|
|
|
auth.GET("/ddns/providers", commonHandler(listProviders))
|
2024-10-22 09:19:30 -04:00
|
|
|
auth.POST("/ddns", commonHandler(createDDNS))
|
|
|
|
auth.PATCH("/ddns/:id", commonHandler(updateDDNS))
|
2024-10-21 11:00:51 -04:00
|
|
|
auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS))
|
2024-10-24 09:33:36 -04:00
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/nat", listHandler(listNAT))
|
2024-10-26 11:57:47 -04:00
|
|
|
auth.POST("/nat", commonHandler(createNAT))
|
|
|
|
auth.PATCH("/nat/:id", commonHandler(updateNAT))
|
|
|
|
auth.POST("/batch-delete/nat", commonHandler(batchDeleteNAT))
|
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.GET("/waf", pCommonHandler(listBlockedAddress))
|
|
|
|
auth.POST("/batch-delete/waf", adminHandler(batchDeleteBlockedAddress))
|
2024-11-23 03:22:22 -05:00
|
|
|
|
2024-12-21 12:08:07 -05:00
|
|
|
auth.GET("/online-user", pCommonHandler(listOnlineUser))
|
2024-12-22 10:18:08 -05:00
|
|
|
auth.POST("/online-user/batch-block", adminHandler(batchBlockOnlineUser))
|
2024-12-21 12:08:07 -05:00
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
auth.PATCH("/setting", adminHandler(updateConfig))
|
2024-10-27 01:10:07 -04:00
|
|
|
|
2024-12-06 12:18:34 -05:00
|
|
|
r.NoRoute(fallbackToFrontend(frontendDist))
|
2019-12-08 03:59:58 -05:00
|
|
|
}
|
2022-04-29 21:32:57 -04:00
|
|
|
|
2024-10-19 23:47:45 -04:00
|
|
|
func recordPath(c *gin.Context) {
|
|
|
|
url := c.Request.URL.String()
|
|
|
|
for _, p := range c.Params {
|
|
|
|
url = strings.Replace(url, p.Value, ":"+p.Key, 1)
|
|
|
|
}
|
|
|
|
c.Set("MatchedPath", url)
|
|
|
|
}
|
2024-10-21 02:30:50 -04:00
|
|
|
|
2024-10-21 11:00:51 -04:00
|
|
|
func newErrorResponse(err error) model.CommonResponse[any] {
|
|
|
|
return model.CommonResponse[any]{
|
2024-10-21 02:30:50 -04:00
|
|
|
Success: false,
|
|
|
|
Error: err.Error(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-23 05:56:51 -04:00
|
|
|
type handlerFunc[T any] func(c *gin.Context) (T, error)
|
2024-12-21 11:05:41 -05:00
|
|
|
type pHandlerFunc[S ~[]E, E any] func(c *gin.Context) (*model.Value[S], error)
|
2024-10-21 02:30:50 -04:00
|
|
|
|
|
|
|
// There are many error types in gorm, so create a custom type to represent all
|
|
|
|
// gorm errors here instead
|
|
|
|
type gormError struct {
|
|
|
|
msg string
|
|
|
|
a []interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func newGormError(format string, args ...interface{}) error {
|
|
|
|
return &gormError{
|
|
|
|
msg: format,
|
|
|
|
a: args,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ge *gormError) Error() string {
|
|
|
|
return fmt.Sprintf(ge.msg, ge.a...)
|
|
|
|
}
|
|
|
|
|
2024-10-25 09:45:05 -04:00
|
|
|
type wsError struct {
|
|
|
|
msg string
|
|
|
|
a []interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func newWsError(format string, args ...interface{}) error {
|
|
|
|
return &wsError{
|
|
|
|
msg: format,
|
|
|
|
a: args,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (we *wsError) Error() string {
|
|
|
|
return fmt.Sprintf(we.msg, we.a...)
|
|
|
|
}
|
|
|
|
|
2024-10-23 05:56:51 -04:00
|
|
|
func commonHandler[T any](handler handlerFunc[T]) func(*gin.Context) {
|
2024-10-21 02:30:50 -04:00
|
|
|
return func(c *gin.Context) {
|
2024-12-21 11:05:41 -05:00
|
|
|
handle(c, handler)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func adminHandler[T any](handler handlerFunc[T]) func(*gin.Context) {
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
auth, ok := c.Get(model.CtxKeyAuthorizedUser)
|
|
|
|
if !ok {
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("unauthorized")))
|
2024-10-23 05:56:51 -04:00
|
|
|
return
|
|
|
|
}
|
2024-12-21 11:05:41 -05:00
|
|
|
|
|
|
|
user := *auth.(*model.User)
|
|
|
|
if user.Role != model.RoleAdmin {
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("permission denied")))
|
2024-10-23 05:56:51 -04:00
|
|
|
return
|
2024-12-21 11:05:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
handle(c, handler)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handle[T any](c *gin.Context, handler handlerFunc[T]) {
|
|
|
|
data, err := handler(c)
|
|
|
|
if err == nil {
|
|
|
|
c.JSON(http.StatusOK, model.CommonResponse[T]{Success: true, Data: data})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch err.(type) {
|
|
|
|
case *gormError:
|
|
|
|
log.Printf("NEZHA>> gorm error: %v", err)
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(singleton.Localizer.ErrorT("database error")))
|
|
|
|
return
|
|
|
|
case *wsError:
|
|
|
|
// Connection is upgraded to WebSocket, so c.Writer is no longer usable
|
|
|
|
if msg := err.Error(); msg != "" {
|
|
|
|
log.Printf("NEZHA>> websocket error: %v", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func listHandler[S ~[]E, E model.CommonInterface](handler handlerFunc[S]) func(*gin.Context) {
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
data, err := handler(c)
|
|
|
|
if err != nil {
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(err))
|
2024-10-25 09:45:05 -04:00
|
|
|
return
|
2024-12-21 11:05:41 -05:00
|
|
|
}
|
|
|
|
|
2024-12-24 10:23:01 -05:00
|
|
|
filtered := filter(c, data)
|
|
|
|
c.JSON(http.StatusOK, model.CommonResponse[S]{Success: true, Data: model.SearchByIDCtx(c, filtered)})
|
2024-12-21 11:05:41 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func pCommonHandler[S ~[]E, E any](handler pHandlerFunc[S, E]) func(*gin.Context) {
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
data, err := handler(c)
|
|
|
|
if err != nil {
|
2024-10-23 05:56:51 -04:00
|
|
|
c.JSON(http.StatusOK, newErrorResponse(err))
|
|
|
|
return
|
2024-10-21 02:30:50 -04:00
|
|
|
}
|
2024-12-21 11:05:41 -05:00
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.PaginatedResponse[S, E]{Success: true, Data: data})
|
2024-10-21 02:30:50 -04:00
|
|
|
}
|
|
|
|
}
|
2024-10-24 09:33:36 -04:00
|
|
|
|
2024-12-21 11:05:41 -05:00
|
|
|
func filter[S ~[]E, E model.CommonInterface](ctx *gin.Context, s S) S {
|
|
|
|
return slices.DeleteFunc(s, func(e E) bool {
|
|
|
|
return !e.HasPermission(ctx)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func getUid(c *gin.Context) uint64 {
|
|
|
|
user, _ := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
|
|
|
|
return user.ID
|
|
|
|
}
|
|
|
|
|
2024-12-06 12:18:34 -05:00
|
|
|
func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) {
|
2024-11-29 08:31:39 -05:00
|
|
|
checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string) bool {
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
c.File(path)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
f, err := fs.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
fileStat, err := f.Stat()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
2024-11-29 09:49:17 -05:00
|
|
|
if fileStat.IsDir() {
|
|
|
|
return false
|
|
|
|
}
|
2024-11-29 08:31:39 -05:00
|
|
|
http.ServeContent(c.Writer, c.Request, path, fileStat.ModTime(), f.(io.ReadSeeker))
|
|
|
|
return true
|
2024-10-24 09:33:36 -04:00
|
|
|
}
|
2024-11-29 08:31:39 -05:00
|
|
|
return func(c *gin.Context) {
|
|
|
|
if strings.HasPrefix(c.Request.URL.Path, "/api") {
|
|
|
|
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
2024-10-24 09:33:36 -04:00
|
|
|
return
|
|
|
|
}
|
2024-11-29 08:31:39 -05:00
|
|
|
if strings.HasPrefix(c.Request.URL.Path, "/dashboard") {
|
|
|
|
stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard")
|
2024-12-10 09:27:06 -05:00
|
|
|
localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath)
|
2024-12-06 12:18:34 -05:00
|
|
|
if checkLocalFileOrFs(c, frontendDist, localFilePath) {
|
2024-11-29 08:31:39 -05:00
|
|
|
return
|
|
|
|
}
|
2024-12-10 09:27:06 -05:00
|
|
|
if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.AdminTemplate+"/index.html") {
|
2024-11-29 08:31:39 -05:00
|
|
|
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2024-12-06 10:19:28 -05:00
|
|
|
localFilePath := path.Join(singleton.Conf.UserTemplate, c.Request.URL.Path)
|
2024-12-06 12:18:34 -05:00
|
|
|
if checkLocalFileOrFs(c, frontendDist, localFilePath) {
|
2024-11-29 08:31:39 -05:00
|
|
|
return
|
|
|
|
}
|
2024-12-06 12:18:34 -05:00
|
|
|
if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.UserTemplate+"/index.html") {
|
2024-11-29 08:31:39 -05:00
|
|
|
c.JSON(http.StatusOK, newErrorResponse(errors.New("404 Not Found")))
|
|
|
|
}
|
2024-10-24 09:33:36 -04:00
|
|
|
}
|
|
|
|
}
|