500 lines
14 KiB
Go
500 lines
14 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/gzip"
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gdy666/lucky/config"
|
|
"github.com/gdy666/lucky/socketproxy"
|
|
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
|
|
"github.com/gdy666/lucky/thirdlib/gdylib/ginutils"
|
|
"github.com/gdy666/lucky/thirdlib/gdylib/logsbuffer"
|
|
"github.com/gdy666/lucky/thirdlib/gdylib/netinterfaces"
|
|
"github.com/gdy666/lucky/thirdlib/gdylib/stringsp"
|
|
"github.com/golang-jwt/jwt"
|
|
"github.com/shirou/gopsutil/v3/cpu"
|
|
"github.com/shirou/gopsutil/v3/mem"
|
|
"github.com/shirou/gopsutil/v3/process"
|
|
)
|
|
|
|
//go:embed adminviews/dist
|
|
var staticFs embed.FS
|
|
var stafs fs.FS
|
|
var loginErrorCount = int32(0)
|
|
var rebootOnce sync.Once
|
|
var logBuffer *logsbuffer.LogsBuffer
|
|
|
|
type LogItem struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Content string `json:"log"`
|
|
}
|
|
|
|
func logConvert(lg *logsbuffer.LogItem) any {
|
|
l := LogItem{Content: lg.Content, Timestamp: fmt.Sprintf("%d", lg.Timestamp)}
|
|
return l
|
|
}
|
|
|
|
func init() {
|
|
stafs, _ = fs.Sub(staticFs, "adminviews/dist")
|
|
logBuffer = logsbuffer.Create(1024)
|
|
//logBuffer.SetLogItemConverFunc(logConvert)
|
|
log.SetOutput(io.MultiWriter(logBuffer, os.Stdout))
|
|
|
|
}
|
|
|
|
func RunAdminWeb(conf *config.BaseConfigure) {
|
|
|
|
//gin.Default()
|
|
logBuffer.SetBufferSize(conf.LogMaxSize)
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.New()
|
|
|
|
if gin.Mode() != gin.ReleaseMode {
|
|
r.Use(gin.Logger(), gin.Recovery())
|
|
} else {
|
|
r.Use(gin.Recovery())
|
|
}
|
|
|
|
r.Use(checkLocalIP)
|
|
r.Use(gzip.Gzip(gzip.DefaultCompression))
|
|
r.Use(ginutils.Cors())
|
|
|
|
r.Use(ginutils.HandlerStaticFiles(stafs))
|
|
|
|
//r.Use(sessionCheck())
|
|
//r.StaticFS("/", http.FS(stafs))
|
|
|
|
authorized := r.Group("/")
|
|
authorized.Use(tokenCheck())
|
|
{
|
|
authorized.GET("/api/logs", Logs)
|
|
authorized.GET("/api/status", status)
|
|
authorized.GET("/api/test", test)
|
|
|
|
authorized.GET("/api/portforwards", PortForwardsRuleList)
|
|
authorized.POST("/api/portforward", PortForwardsRuleAdd)
|
|
authorized.DELETE("/api/portforward", PortForwardsRuleDelete)
|
|
authorized.PUT("/api/portforward", PortForwardsRuleAlter)
|
|
authorized.GET("/api/portforward/enable", PortForwardsRuleEnable)
|
|
authorized.GET("/api/portforward/configure", portforwardConfigure)
|
|
authorized.PUT("/api/portforward/configure", alterPortForwardConfigure)
|
|
authorized.GET("/api/portforward/logs", getPortwardRuleLogs)
|
|
|
|
authorized.GET("/api/baseconfigure", baseconfigure)
|
|
authorized.PUT("/api/baseconfigure", alterBaseConfigure)
|
|
authorized.GET("/api/reboot_program", rebootProgram)
|
|
|
|
authorized.GET("/api/whitelist/configure", whitelistConfigure)
|
|
authorized.PUT("/api/whitelist/configure", alterWhitelistConfigure)
|
|
authorized.GET("/api/whitelist", querywhitelist)
|
|
authorized.PUT("/api/whitelist/flush", flushwhitelist)
|
|
authorized.DELETE("/api/whitelist", deletewhitelist)
|
|
authorized.GET("/api/blacklist", queryblacklist)
|
|
authorized.PUT("/api/blacklist/flush", flushblacklist)
|
|
authorized.DELETE("/api/blacklist", deleteblacklist)
|
|
|
|
authorized.POST("/api/ddns", addDDNS)
|
|
authorized.PUT("/api/ddns", alterDDNSTask)
|
|
authorized.GET("/api/ddnstasklist", ddnsTaskList)
|
|
authorized.DELETE("/api/ddns", deleteDDNSTask)
|
|
authorized.GET("/api/ddns/enable", enableddns)
|
|
authorized.GET("/api/ddns/configure", ddnsconfigure)
|
|
authorized.PUT("/api/ddns/configure", alterDDNSConfigure)
|
|
|
|
authorized.GET("/api/netinterfaces", getNetinterfaces)
|
|
authorized.GET("/api/ipregtest", IPRegTest)
|
|
authorized.POST("/api/webhooktest", webhookTest)
|
|
|
|
authorized.GET("/api/reverseproxyrules", reverseProxys)
|
|
authorized.POST("/api/reverseproxyrule", addReverseProxyRule)
|
|
authorized.PUT("/api/reverseproxyrule", alterReverseProxyRule)
|
|
authorized.DELETE("/api/reverseproxyrule", deleteReverseProxyRule)
|
|
authorized.GET("/api/reverseproxyrule/enable", enableReverseProxyRule)
|
|
authorized.GET("/api/reverseproxyrule/logs", getReverseProxyLog)
|
|
|
|
authorized.POST("/api/ssl", addSSL)
|
|
authorized.GET("/api/ssl", getSSLCertficateList)
|
|
authorized.PUT("/api/ssl", alterSSLCertficate)
|
|
authorized.DELETE("/api/ssl", deleteSSLCertficate)
|
|
|
|
authorized.POST("/api/wol/device", addWOLDevice)
|
|
authorized.GET("/api/wol/device/wakeup", WOLDeviceWakeUp)
|
|
authorized.GET("/api/wol/devices", getWOLDeviceList)
|
|
authorized.PUT("/api/wol/device", alterWOLDevice)
|
|
authorized.DELETE("/api/wol/device", deleteWOLDevice)
|
|
|
|
authorized.GET("/api/info", info)
|
|
authorized.GET("/api/configure", configure)
|
|
authorized.POST("/api/configure", restoreConfigure)
|
|
authorized.POST("/api/getfilebase64", getFileBase64)
|
|
|
|
authorized.GET("/api/restoreconfigureconfirm", restoreConfigureConfirm)
|
|
r.PUT("/api/logout", logout)
|
|
}
|
|
r.POST("/api/login", login)
|
|
//r.GET("/FreeOSMemory", FreeOSMemory)
|
|
|
|
r.GET("/wl", whitelistBasicAuth, whilelistAdd)
|
|
r.GET("/wl/:url", whitelistBasicAuth, whilelistAdd)
|
|
r.GET("/version", queryVersion)
|
|
|
|
//r.Use(func() *gin.Context {})
|
|
|
|
go func() {
|
|
httpListen := fmt.Sprintf(":%d", conf.AdminWebListenPort)
|
|
log.Printf("AdminWeb(Http) listen on %s", httpListen)
|
|
err := r.Run(httpListen)
|
|
if err != nil {
|
|
log.Printf("Admin Http Listen error:%s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
if conf.AdminWebListenTLS {
|
|
certlist := config.GetValidSSLCertficateList()
|
|
if len(certlist) <= 0 {
|
|
log.Printf("可用SSL证书列表为空,AdminWeb(Https) 监听服务中止运行")
|
|
return
|
|
}
|
|
httpsListen := fmt.Sprintf(":%d", conf.AdminWebListenHttpsPort)
|
|
|
|
server := &http.Server{
|
|
Addr: httpsListen,
|
|
Handler: r,
|
|
}
|
|
server.TLSConfig = &tls.Config{}
|
|
server.TLSConfig.Certificates = certlist
|
|
ln, err := net.Listen("tcp", httpsListen)
|
|
if err != nil {
|
|
log.Fatalf("Admin Https Listen error:%s", err.Error())
|
|
}
|
|
log.Printf("AdminWeb(Https) listen on %s", httpsListen)
|
|
err = server.ServeTLS(ln, "", "")
|
|
if err != nil {
|
|
log.Printf("AdminWeb(Https) Server error:%s", err.Error())
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// func FreeOSMemory(c *gin.Context) {
|
|
// debug.FreeOSMemory()
|
|
// c.JSON(http.StatusOK, gin.H{"ret": 0})
|
|
// }
|
|
|
|
// Logs web
|
|
func Logs(c *gin.Context) {
|
|
preTimeStampStr := c.Query("pre")
|
|
preTimeStamp, _ := strconv.ParseInt(preTimeStampStr, 10, 64)
|
|
logs := logBuffer.GetLogs(logConvert, preTimeStamp)
|
|
|
|
logCount := logBuffer.GetLogCount()
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ret": 0,
|
|
"logs": logs,
|
|
"logsCount": logCount,
|
|
})
|
|
|
|
}
|
|
|
|
func info(c *gin.Context) {
|
|
info := config.GetAppInfo()
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "info": *info})
|
|
|
|
}
|
|
|
|
func logout(c *gin.Context) {
|
|
config.FlushLoginRandomKey()
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": "已注销登录"})
|
|
}
|
|
|
|
func queryVersion(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "version": config.GetVersion()})
|
|
}
|
|
|
|
func checkLocalIP(c *gin.Context) {
|
|
clientIP := c.ClientIP()
|
|
//fmt.Printf("clientIP:%s\n", clientIP)
|
|
bc := config.GetBaseConfigure()
|
|
|
|
if !isLocalIP(clientIP) && !bc.AllowInternetaccess {
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"ret": 1,
|
|
"msg": "外网禁止访问,如果需要允许外网访问请在后台设置中打开相应开关.",
|
|
"ip": clientIP})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
func tokenCheck() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
|
|
// if config.GetRunMode() == "dev" {
|
|
// c.Next()
|
|
// return
|
|
// }
|
|
|
|
tokenString, _ := c.GetQuery("Authorization")
|
|
if tokenString == "" {
|
|
tokenString = c.GetHeader("Authorization")
|
|
}
|
|
|
|
token, err := ginutils.GetJWTToken(tokenString, "strings")
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusOK, gin.H{"ret": -1, "msg": "登录失效"})
|
|
return
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
c.AbortWithStatusJSON(http.StatusOK, gin.H{"ret": -1, "msg": "登录失效"})
|
|
return
|
|
}
|
|
|
|
account := claims["account"].(string)
|
|
password := claims["password"].(string)
|
|
loginKey := claims["loginkey"].(string)
|
|
|
|
if account == "" || password == "" {
|
|
c.AbortWithStatusJSON(http.StatusOK, gin.H{"ret": -1, "msg": "登录失效"})
|
|
return
|
|
}
|
|
|
|
bc := config.GetBaseConfigure()
|
|
|
|
// //fmt.Printf("session中的account:%s password:%s\n", account, password)
|
|
if bc.AdminAccount != account || bc.AdminPassword != password || loginKey != config.GetLoginRandomKey() {
|
|
c.AbortWithStatusJSON(http.StatusOK, gin.H{"ret": -1, "msg": "登录失效"})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func rebootProgram(c *gin.Context) {
|
|
rebootOnce.Do(func() {
|
|
go func() {
|
|
fileutils.OpenProgramOrFile(os.Args)
|
|
os.Exit(0)
|
|
}()
|
|
})
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": ""})
|
|
}
|
|
|
|
func login(c *gin.Context) {
|
|
var requestObj struct {
|
|
Account string `json:"Account"`
|
|
Password string `json:"Password"`
|
|
}
|
|
err := c.BindJSON(&requestObj)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "登录失败,登录请求解析出错"})
|
|
return
|
|
}
|
|
|
|
if atomic.LoadInt32(&loginErrorCount) >= 99 {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "登录错误次数太多,后台登录功能已禁用,请重启程序."})
|
|
return
|
|
}
|
|
|
|
bc := config.GetBaseConfigure()
|
|
|
|
if bc.AdminAccount != requestObj.Account || bc.AdminPassword != requestObj.Password {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "登录失败,账号或密码有误"})
|
|
atomic.AddInt32(&loginErrorCount, 1)
|
|
return
|
|
}
|
|
|
|
config.FlushLoginRandomKey()
|
|
tokenInfo := make(map[string]interface{})
|
|
tokenInfo["account"] = requestObj.Account //用户名
|
|
tokenInfo["password"] = requestObj.Password
|
|
tokenInfo["loginkey"] = config.GetLoginRandomKey()
|
|
tokenString, err := ginutils.GetJWTTokenString(tokenInfo, "strings", time.Hour*24)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "登录失败,token生成出错"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": "登录成功", "token": tokenString})
|
|
}
|
|
|
|
func alterBaseConfigure(c *gin.Context) {
|
|
var requestObj config.BaseConfigure
|
|
err := c.BindJSON(&requestObj)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "请求解析出错"})
|
|
return
|
|
}
|
|
requestObj.AdminAccount = strings.TrimSpace(requestObj.AdminAccount)
|
|
|
|
if len(requestObj.AdminAccount) == 0 || len(requestObj.AdminPassword) == 0 {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "账号或密码不能为空"})
|
|
return
|
|
}
|
|
|
|
preBaseConfigure := config.GetBaseConfigure()
|
|
if preBaseConfigure.AdminWebListenPort != requestObj.AdminWebListenPort && !config.CheckTCPPortAvalid(requestObj.AdminWebListenPort) { //检测新端口
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("新的后端管理监听端口[%d]已被占用,修改设置失败", requestObj.AdminWebListenPort)})
|
|
return
|
|
}
|
|
|
|
err = config.SetBaseConfigure(&requestObj)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 2, "msg": "保存配置过程发生错误,请检测相关启动配置"})
|
|
return
|
|
}
|
|
|
|
if preBaseConfigure.LogMaxSize != requestObj.LogMaxSize {
|
|
logBuffer.SetBufferSize(requestObj.LogMaxSize)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0})
|
|
}
|
|
|
|
func baseconfigure(c *gin.Context) {
|
|
conf := config.GetBaseConfigure()
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "baseconfigure": conf})
|
|
}
|
|
|
|
func getNetinterfaces(c *gin.Context) {
|
|
ipv4NetInterfaces, ipv6Netinterfaces, err := netinterfaces.GetNetInterface()
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("获取网卡列表出错:%s", err.Error())})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "data": gin.H{"IPv6NewInterfaces": ipv6Netinterfaces, "IPv4NewInterfaces": ipv4NetInterfaces}})
|
|
}
|
|
|
|
func IPRegTest(c *gin.Context) {
|
|
iptype := c.Query("iptype")
|
|
netinterface := c.Query("netinterface")
|
|
ipreg := c.Query("ipreg")
|
|
|
|
ip := netinterfaces.GetIPFromNetInterface(iptype, netinterface, ipreg)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0, "ip": ip})
|
|
}
|
|
|
|
func test(c *gin.Context) {
|
|
|
|
c.JSON(http.StatusOK, gin.H{"ret": 0})
|
|
}
|
|
|
|
func status(c *gin.Context) {
|
|
|
|
v, _ := mem.VirtualMemory()
|
|
|
|
currentProcessMem := GetCurrentProcessMem()
|
|
//fmt.Fprintf(w, "当前进程 CPU使用率:%.2f%% 协程数:%d 进程内存使用:%s 系统内存总量:%s 已用:%s 可用:%s \n", GetCurrentProcessCPUPrecent(), runtime.NumGoroutine(), formatFileSize(currentProcessMem), formatFileSize(v.Total), formatFileSize(v.Used), formatFileSize(v.Free))
|
|
//fmt.Fprintf(w, "当前全局TCP 连接数:%d 全局TCP连接数最大限制:%d\n", core.GetGlobalTCPConns(), core.GetGlobalMaxConnections())
|
|
|
|
//var proxyStatusList []string
|
|
|
|
// for _, p := range *config.GlobalProxy {
|
|
// //fmt.Fprintf(w, "%s\n", p.GetStatus())
|
|
// proxyStatusList = append(proxyStatusList, p.GetStatus())
|
|
// }
|
|
|
|
appInfo := config.GetAppInfo()
|
|
|
|
respMap := make(map[string]interface{})
|
|
respMap["totleMem"] = stringsp.BinaryUnitToStr(v.Total)
|
|
respMap["usedMem"] = stringsp.BinaryUnitToStr(v.Used)
|
|
respMap["unusedMem"] = stringsp.BinaryUnitToStr(v.Free)
|
|
respMap["currentProcessUsedCPU"] = fmt.Sprintf("%.2f%%", GetCurrentProcessCPUPrecent())
|
|
respMap["goroutine"] = fmt.Sprintf("%d", runtime.NumGoroutine())
|
|
respMap["processUsedMem"] = stringsp.BinaryUnitToStr(currentProcessMem)
|
|
respMap["currentTCPConnections"] = fmt.Sprintf("%d", socketproxy.GetGlobalTCPPortForwardConnections())
|
|
respMap["currentUDPConnections"] = fmt.Sprintf("%d", socketproxy.GetGlobalUDPPortForwardGroutineCount())
|
|
respMap["maxTCPConnections"] = fmt.Sprintf("%d", socketproxy.GetGlobalTCPPortforwardMaxConnections())
|
|
respMap["usedCPU"] = fmt.Sprintf("%.2f%%", GetCpuPercent())
|
|
respMap["runTime"] = appInfo.RunTime
|
|
//respMap["proxysStatus"] = proxyStatusList
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"ret": 0,
|
|
"data": respMap,
|
|
})
|
|
}
|
|
|
|
func GetCurrentProcessMem() uint64 {
|
|
plist, e := process.Processes()
|
|
if e == nil {
|
|
for _, p := range plist {
|
|
if int(p.Pid) == os.Getpid() {
|
|
mem, err := p.MemoryInfo()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return mem.RSS
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func GetCurrentProcessCPUPrecent() float64 {
|
|
plist, e := process.Processes()
|
|
if e == nil {
|
|
for _, p := range plist {
|
|
if int(p.Pid) == os.Getpid() {
|
|
cpuprecent, err := p.CPUPercent()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return cpuprecent
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func GetCpuPercent() float64 {
|
|
percent, _ := cpu.Percent(time.Second, false)
|
|
return percent[0]
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------
|
|
|
|
func isLocalIP(ipstr string) bool {
|
|
|
|
ip := net.ParseIP(ipstr)
|
|
|
|
if ip.IsLoopback() {
|
|
return true
|
|
}
|
|
|
|
ip4 := ip.To4()
|
|
if ip4 == nil {
|
|
return false
|
|
}
|
|
|
|
return ip4[0] == 10 || // 10.0.0.0/8
|
|
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12
|
|
(ip4[0] == 169 && ip4[1] == 254) || // 169.254.0.0/16
|
|
(ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16
|
|
}
|