1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/zhaojh329-rttys

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
api.go 14 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Jianhui Zhao Отправлено 23.11.2024 16:42 dee7677
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
package main
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"rttys/cache"
"rttys/config"
"rttys/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
var httpSessions *cache.Cache
//go:embed ui/dist
var staticFs embed.FS
func allowOrigin(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("content-type", "application/json")
}
func httpLogin(cfg *config.Config, creds *credentials) bool {
if creds.Username == "" || creds.Password == "" {
return false
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return false
}
defer db.Close()
cnt := 0
db.QueryRow("SELECT COUNT(*) FROM account WHERE username = ? AND password = ?", creds.Username, creds.Password).Scan(&cnt)
return cnt != 0
}
func devInWhiteList(devid string, cfg *config.Config) bool {
if cfg.WhiteList == nil {
return true
}
_, ok := cfg.WhiteList[devid]
return ok
}
func isLocalRequest(c *gin.Context) bool {
addr, _ := net.ResolveTCPAddr("tcp", c.Request.RemoteAddr)
return addr.IP.IsLoopback()
}
func httpAuth(cfg *config.Config, c *gin.Context) bool {
if !cfg.LocalAuth && isLocalRequest(c) {
return true
}
cookie, err := c.Cookie("sid")
if err != nil || !httpSessions.Have(cookie) {
return false
}
httpSessions.Active(cookie, 0)
return true
}
func isAdminUsername(cfg *config.Config, username string) bool {
if username == "" {
return false
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return false
}
defer db.Close()
isAdmin := false
if db.QueryRow("SELECT admin FROM account WHERE username = ?", username).Scan(&isAdmin) == sql.ErrNoRows {
return false
}
return isAdmin
}
func devMatchUser(devid string, username string, cfg *config.Config) bool {
if username == "" {
return false
}
if isAdminUsername(cfg, username) {
return true
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return false
}
defer db.Close()
cnt := 0
if db.QueryRow("SELECT count(id) FROM device WHERE id = ? AND username == ?", devid, username).Scan(&cnt) == sql.ErrNoRows {
return false
}
return cnt > 0
}
func getLoginUsername(c *gin.Context) string {
cookie, err := c.Cookie("sid")
if err != nil {
return ""
}
username, ok := httpSessions.Get(cookie)
if ok {
return username.(string)
}
return ""
}
func apiStart(br *broker) {
cfg := br.cfg
httpSessions = cache.New(30*time.Minute, 5*time.Second)
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
authorized := r.Group("/", func(c *gin.Context) {
isConnect := false
devid := ""
if strings.HasPrefix(c.Request.URL.Path, "/connect/") {
devid = c.Param("devid")
if devid == "" {
c.AbortWithStatus(http.StatusBadRequest)
return
}
if devInWhiteList(devid, cfg) {
return
}
isConnect = true
}
if !httpAuth(cfg, c) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if isConnect && !devMatchUser(devid, getLoginUsername(c), cfg) {
c.AbortWithStatus(http.StatusUnauthorized)
}
})
authorized.GET("/fontsize", func(c *gin.Context) {
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
defer db.Close()
value := "16"
db.QueryRow("SELECT value FROM config WHERE name = 'FontSize'").Scan(&value)
FontSize, _ := strconv.Atoi(value)
c.JSON(http.StatusOK, gin.H{"size": FontSize})
})
authorized.POST("/fontsize", func(c *gin.Context) {
data := make(map[string]int)
err := c.BindJSON(&data)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
size, ok := data["size"]
if !ok {
c.Status(http.StatusBadRequest)
return
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
defer db.Close()
if size < 12 {
size = 12
}
_, err = db.Exec("DELETE FROM config WHERE name = 'FontSize'")
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
_, err = db.Exec("INSERT INTO config values('FontSize',?)", fmt.Sprintf("%d", size))
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
})
authorized.GET("/connect/:devid", func(c *gin.Context) {
if c.GetHeader("Upgrade") != "websocket" {
c.Redirect(http.StatusFound, "/rtty/"+c.Param("devid"))
return
}
serveUser(br, c)
})
authorized.GET("/devs", func(c *gin.Context) {
type DeviceInfo struct {
ID string `json:"id"`
Connected uint32 `json:"connected"`
Uptime uint32 `json:"uptime"`
Description string `json:"description"`
Bound bool `json:"bound"`
Online bool `json:"online"`
Proto uint8 `json:"proto"`
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
defer db.Close()
sql := "SELECT id, description, username FROM device"
if cfg.LocalAuth || !isLocalRequest(c) {
username := getLoginUsername(c)
if username == "" {
c.Status(http.StatusUnauthorized)
return
}
if !isAdminUsername(cfg, username) {
sql += fmt.Sprintf(" WHERE username = '%s'", username)
}
}
devs := make([]DeviceInfo, 0)
rows, err := db.Query(sql)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
for rows.Next() {
id := ""
desc := ""
username := ""
err := rows.Scan(&id, &desc, &username)
if err != nil {
log.Error().Msg(err.Error())
break
}
di := DeviceInfo{
ID: id,
Description: desc,
Bound: username != "",
}
if dev, ok := br.devices[id]; ok {
dev := dev.(*device)
di.Connected = uint32(time.Now().Unix() - dev.timestamp)
di.Uptime = dev.uptime
di.Online = true
di.Proto = dev.proto
}
devs = append(devs, di)
}
allowOrigin(c.Writer)
c.JSON(http.StatusOK, devs)
})
authorized.POST("/cmd/:devid", func(c *gin.Context) {
allowOrigin(c.Writer)
handleCmdReq(br, c)
})
r.Any("/web/:devid/:proto/:addr/*path", func(c *gin.Context) {
httpProxyRedirect(br, c)
})
r.GET("/authorized/:devid", func(c *gin.Context) {
devid := c.Param("devid")
authorized := false
if devInWhiteList(devid, cfg) {
authorized = true
}
if !authorized && httpAuth(cfg, c) {
username := getLoginUsername(c)
authorized = devMatchUser(devid, username, cfg)
}
c.JSON(http.StatusOK, gin.H{
"authorized": authorized,
})
})
r.POST("/signin", func(c *gin.Context) {
var creds credentials
err := c.BindJSON(&creds)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
if httpLogin(cfg, &creds) {
sid := utils.GenUniqueID("http")
httpSessions.Set(sid, creds.Username, 0)
c.SetCookie("sid", sid, 0, "", "", false, true)
c.JSON(http.StatusOK, gin.H{
"sid": sid,
"username": creds.Username,
})
return
}
c.Status(http.StatusForbidden)
})
r.GET("/alive", func(c *gin.Context) {
if !httpAuth(cfg, c) {
c.AbortWithStatus(http.StatusUnauthorized)
} else {
c.Status(http.StatusOK)
}
})
r.GET("/signout", func(c *gin.Context) {
cookie, err := c.Cookie("sid")
if err != nil || !httpSessions.Have(cookie) {
return
}
httpSessions.Del(cookie)
c.Status(http.StatusOK)
})
r.POST("/signup", func(c *gin.Context) {
var creds credentials
err := c.BindJSON(&creds)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
if cfg.DisableSignUp {
c.Status(http.StatusForbidden)
return
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
defer db.Close()
isAdmin := 0
cnt := 0
db.QueryRow("SELECT COUNT(*) FROM account").Scan(&cnt)
if cnt == 0 {
isAdmin = 1
}
db.QueryRow("SELECT COUNT(*) FROM account WHERE username = ?", creds.Username).Scan(&cnt)
if cnt > 0 {
c.Status(http.StatusForbidden)
return
}
_, err = db.Exec("INSERT INTO account values(?,?,?)", creds.Username, creds.Password, isAdmin)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
})
r.GET("/isadmin", func(c *gin.Context) {
isAdmin := true
if cfg.LocalAuth || !isLocalRequest(c) {
isAdmin = isAdminUsername(cfg, getLoginUsername(c))
}
c.JSON(http.StatusOK, gin.H{"admin": isAdmin})
})
r.GET("/allowsignup", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"allow": !cfg.DisableSignUp})
})
r.GET("/users", func(c *gin.Context) {
loginUsername := getLoginUsername(c)
isAdmin := isAdminUsername(cfg, loginUsername)
if cfg.LocalAuth || !isLocalRequest(c) {
if !isAdmin {
c.Status(http.StatusUnauthorized)
return
}
}
users := []string{}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
defer db.Close()
rows, err := db.Query("SELECT username FROM account")
if err != nil {
log.Error().Msg(err.Error())
c.Status(http.StatusInternalServerError)
return
}
for rows.Next() {
username := ""
err := rows.Scan(&username)
if err != nil {
log.Error().Msg(err.Error())
break
}
if isAdmin && username == loginUsername {
continue
}
users = append(users, username)
}
c.JSON(http.StatusOK, gin.H{"users": users})
})
r.POST("/bind", func(c *gin.Context) {
if cfg.LocalAuth || !isLocalRequest(c) {
username := getLoginUsername(c)
if !isAdminUsername(cfg, username) {
c.Status(http.StatusUnauthorized)
return
}
}
type binddata struct {
Username string `json:"username"`
Devices []string `json:"devices"`
}
data := binddata{}
err := c.BindJSON(&data)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return
}
defer db.Close()
isAdmin := false
if db.QueryRow("SELECT admin FROM account WHERE username = ?", data.Username).Scan(&isAdmin) == sql.ErrNoRows || isAdmin {
c.Status(http.StatusOK)
return
}
for _, devid := range data.Devices {
db.Exec("UPDATE device SET username = ? WHERE id = ?", data.Username, devid)
}
c.Status(http.StatusOK)
})
r.POST("/unbind", func(c *gin.Context) {
if cfg.LocalAuth || !isLocalRequest(c) {
username := getLoginUsername(c)
if !isAdminUsername(cfg, username) {
c.Status(http.StatusUnauthorized)
return
}
}
type binddata struct {
Devices []string `json:"devices"`
}
data := binddata{}
err := c.BindJSON(&data)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return
}
defer db.Close()
for _, devid := range data.Devices {
db.Exec("UPDATE device SET username = '' WHERE id = ?", devid)
}
c.Status(http.StatusOK)
})
r.POST("/delete", func(c *gin.Context) {
type deldata struct {
Devices []string `json:"devices"`
}
data := deldata{}
err := c.BindJSON(&data)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
db, err := instanceDB(cfg.DB)
if err != nil {
log.Error().Msg(err.Error())
return
}
defer db.Close()
username := ""
if cfg.LocalAuth || !isLocalRequest(c) {
username = getLoginUsername(c)
if isAdminUsername(cfg, username) {
username = ""
}
}
for _, devid := range data.Devices {
if _, ok := br.devices[devid]; !ok {
sql := fmt.Sprintf("DELETE FROM device WHERE id = '%s'", devid)
if username != "" {
sql += fmt.Sprintf(" AND username = '%s'", username)
}
db.Exec(sql)
}
}
c.Status(http.StatusOK)
})
r.GET("/file/:sid", func(c *gin.Context) {
sid := c.Param("sid")
if fp, ok := br.fileProxy.Load(sid); ok {
fp := fp.(*fileProxy)
s := br.sessions[sid]
fp.Ack(s.dev, sid)
defer func() {
if err := recover(); err != nil {
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
fp.reader.Close()
}
}
}
}
}()
c.DataFromReader(http.StatusOK, -1, "application/octet-stream", fp.reader, nil)
br.fileProxy.Delete(sid)
}
})
r.NoRoute(func(c *gin.Context) {
fs, _ := fs.Sub(staticFs, "ui/dist")
path := c.Request.URL.Path
if path != "/" {
f, err := fs.Open(path[1:])
if err != nil {
c.Request.URL.Path = "/"
r.HandleContext(c)
return
}
if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") {
if strings.HasSuffix(path, "css") || strings.HasSuffix(path, "js") {
magic := make([]byte, 2)
f.Read(magic)
if magic[0] == 0x1f && magic[1] == 0x8b {
c.Writer.Header().Set("Content-Encoding", "gzip")
}
}
}
f.Close()
}
http.FileServer(http.FS(fs)).ServeHTTP(c.Writer, c.Request)
})
go func() {
var err error
if cfg.WebUISslCert != "" && cfg.WebUISslKey != "" {
log.Info().Msgf("Listen user on: %s SSL on", cfg.AddrUser)
err = r.RunTLS(cfg.AddrUser, cfg.WebUISslCert, cfg.WebUISslKey)
} else {
log.Info().Msgf("Listen user on: %s SSL off", cfg.AddrUser)
err = r.Run(cfg.AddrUser)
}
log.Fatal().Err(err)
}()
}

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/zhaojh329-rttys.git
git@api.gitlife.ru:oschina-mirror/zhaojh329-rttys.git
oschina-mirror
zhaojh329-rttys
zhaojh329-rttys
master