mirror of
https://github.com/zhaojh329/rttys.git
synced 2026-02-27 09:53:21 +08:00
310 lines
7.1 KiB
Go
310 lines
7.1 KiB
Go
/*
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2019 Jianhui Zhao <zhaojh329@gmail.com>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"rttys/utils"
|
|
|
|
"github.com/fanjindong/go-cache"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var httpSessions = cache.NewMemCache(cache.WithClearInterval(time.Minute))
|
|
|
|
const httpSessionExpire = 30 * time.Minute
|
|
|
|
//go:embed ui/dist
|
|
var staticFs embed.FS
|
|
|
|
func (srv *RttyServer) ListenAPI() error {
|
|
cfg := &srv.cfg
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
r := gin.New()
|
|
|
|
r.Use(func(c *gin.Context) {
|
|
c.Next()
|
|
log.Debug().Msgf("%s - \"%s %s %s %d\"", c.ClientIP(),
|
|
c.Request.Method, c.Request.URL.Path, c.Request.Proto, c.Writer.Status())
|
|
})
|
|
|
|
if cfg.AllowOrigins {
|
|
log.Debug().Msg("Allow all origins")
|
|
r.Use(cors.Default())
|
|
}
|
|
|
|
authorized := r.Group("/", func(c *gin.Context) {
|
|
if !cfg.LocalAuth && isLocalRequest(c) {
|
|
return
|
|
}
|
|
|
|
if !httpAuth(cfg, c) {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
})
|
|
|
|
authorized.GET("/connect/:devid", func(c *gin.Context) {
|
|
if c.GetHeader("Upgrade") != "websocket" {
|
|
group := c.Query("group")
|
|
devid := c.Param("devid")
|
|
if dev := srv.GetDevice(group, devid); dev == nil {
|
|
c.Redirect(http.StatusFound, "/error/offline")
|
|
return
|
|
}
|
|
|
|
c.Redirect(http.StatusFound, "/rtty/"+devid+"?group="+group)
|
|
return
|
|
}
|
|
handleUserConnection(srv, c)
|
|
})
|
|
|
|
authorized.GET("/counts", func(c *gin.Context) {
|
|
count := 0
|
|
|
|
srv.groups.Range(func(key, value any) bool {
|
|
count += int(value.(*DeviceGroup).count.Load())
|
|
return true
|
|
})
|
|
|
|
c.JSON(http.StatusOK, gin.H{"count": count})
|
|
})
|
|
|
|
authorized.GET("/groups", func(c *gin.Context) {
|
|
groups := []string{""}
|
|
|
|
srv.groups.Range(func(key, value any) bool {
|
|
if key != "" {
|
|
groups = append(groups, key.(string))
|
|
}
|
|
return true
|
|
})
|
|
|
|
c.JSON(http.StatusOK, groups)
|
|
})
|
|
|
|
authorized.GET("/devs", func(c *gin.Context) {
|
|
devs := make([]*DeviceInfo, 0)
|
|
g := srv.GetGroup(c.Query("group"), false)
|
|
|
|
if g == nil {
|
|
c.JSON(http.StatusOK, devs)
|
|
return
|
|
}
|
|
|
|
g.devices.Range(func(key, value any) bool {
|
|
dev := value.(*Device)
|
|
|
|
devs = append(devs, &DeviceInfo{
|
|
Group: dev.group,
|
|
ID: dev.id,
|
|
Desc: dev.desc,
|
|
Connected: uint32(time.Now().Unix() - dev.timestamp),
|
|
Uptime: dev.uptime,
|
|
Proto: dev.proto,
|
|
IPaddr: dev.conn.RemoteAddr().(*net.TCPAddr).IP.String(),
|
|
})
|
|
|
|
return true
|
|
})
|
|
|
|
c.JSON(http.StatusOK, devs)
|
|
})
|
|
|
|
authorized.GET("/dev/:devid", func(c *gin.Context) {
|
|
if dev := srv.GetDevice(c.Query("group"), c.Param("devid")); dev != nil {
|
|
info := &DeviceInfo{
|
|
ID: dev.id,
|
|
Desc: dev.desc,
|
|
Connected: uint32(time.Now().Unix() - dev.timestamp),
|
|
Uptime: dev.uptime,
|
|
Proto: dev.proto,
|
|
IPaddr: dev.conn.RemoteAddr().(*net.TCPAddr).IP.String(),
|
|
}
|
|
c.JSON(http.StatusOK, info)
|
|
} else {
|
|
c.Status(http.StatusNotFound)
|
|
}
|
|
})
|
|
|
|
authorized.POST("/cmd/:devid", func(c *gin.Context) {
|
|
cmdInfo := &CommandReqInfo{}
|
|
|
|
err := c.BindJSON(&cmdInfo)
|
|
if err != nil || cmdInfo.Cmd == "" || cmdInfo.Username == "" {
|
|
cmdErrResp(c, rttyCmdErrInvalid)
|
|
return
|
|
}
|
|
|
|
dev := srv.GetDevice(c.Query("group"), c.Param("devid"))
|
|
if dev == nil {
|
|
cmdErrResp(c, rttyCmdErrOffline)
|
|
return
|
|
}
|
|
|
|
dev.handleCmdReq(c, cmdInfo)
|
|
})
|
|
|
|
authorized.Any("/web/:devid/:proto/:addr/*path", func(c *gin.Context) {
|
|
httpProxyRedirect(srv, c, "")
|
|
})
|
|
|
|
authorized.Any("/web2/:group/:devid/:proto/:addr/*path", func(c *gin.Context) {
|
|
group := c.Param("group")
|
|
httpProxyRedirect(srv, c, group)
|
|
})
|
|
|
|
authorized.GET("/signout", func(c *gin.Context) {
|
|
sid, err := c.Cookie("sid")
|
|
if err != nil || !httpSessions.Exists(sid) {
|
|
return
|
|
}
|
|
|
|
httpSessions.Del(sid)
|
|
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
r.POST("/signin", func(c *gin.Context) {
|
|
type credentials struct {
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
creds := credentials{}
|
|
|
|
err := c.BindJSON(&creds)
|
|
if err != nil {
|
|
c.Status(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if httpLogin(cfg, creds.Password) {
|
|
sid := utils.GenUniqueID()
|
|
|
|
httpSessions.Set(sid, true, cache.WithEx(httpSessionExpire))
|
|
|
|
c.SetCookie("sid", sid, 0, "", "", false, true)
|
|
c.Status(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusUnauthorized)
|
|
})
|
|
|
|
r.GET("/alive", func(c *gin.Context) {
|
|
if !httpAuth(cfg, c) {
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
} else {
|
|
c.Status(http.StatusOK)
|
|
}
|
|
})
|
|
|
|
fs, err := fs.Sub(staticFs, "ui/dist")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
root := http.FS(fs)
|
|
fh := http.FileServer(root)
|
|
|
|
r.NoRoute(func(c *gin.Context) {
|
|
upath := path.Clean(c.Request.URL.Path)
|
|
|
|
if strings.HasSuffix(upath, ".js") || strings.HasSuffix(upath, ".css") {
|
|
if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") {
|
|
f, err := root.Open(upath + ".gz")
|
|
if err == nil {
|
|
f.Close()
|
|
|
|
c.Request.URL.Path += ".gz"
|
|
|
|
if strings.HasSuffix(upath, ".js") {
|
|
c.Writer.Header().Set("Content-Type", "application/javascript")
|
|
} else if strings.HasSuffix(upath, ".css") {
|
|
c.Writer.Header().Set("Content-Type", "text/css")
|
|
}
|
|
|
|
c.Writer.Header().Set("Content-Encoding", "gzip")
|
|
}
|
|
}
|
|
} else if upath != "/" {
|
|
f, err := root.Open(upath)
|
|
if err != nil {
|
|
c.Request.URL.Path = "/"
|
|
r.HandleContext(c)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
}
|
|
|
|
fh.ServeHTTP(c.Writer, c.Request)
|
|
})
|
|
|
|
ln, err := net.Listen("tcp", cfg.AddrUser)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ln.Close()
|
|
|
|
log.Info().Msgf("Listen users on: %s", ln.Addr().(*net.TCPAddr))
|
|
|
|
return r.RunListener(ln)
|
|
}
|
|
|
|
func httpLogin(cfg *Config, password string) bool {
|
|
return cfg.Password == "" || cfg.Password == password
|
|
}
|
|
|
|
func isLocalRequest(c *gin.Context) bool {
|
|
addr, _ := net.ResolveTCPAddr("tcp", c.Request.RemoteAddr)
|
|
return addr.IP.IsLoopback()
|
|
}
|
|
|
|
func httpAuth(cfg *Config, c *gin.Context) bool {
|
|
if !cfg.LocalAuth && isLocalRequest(c) {
|
|
return true
|
|
}
|
|
|
|
sid, err := c.Cookie("sid")
|
|
if err != nil || !httpSessions.Exists(sid) {
|
|
return false
|
|
}
|
|
|
|
httpSessions.Expire(sid, httpSessionExpire)
|
|
|
|
return true
|
|
}
|