package main import ( "database/sql" "embed" "fmt" "net" "net/http" "path" "strconv" "strings" "time" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "github.com/rs/zerolog/log" "github.com/zhaojh329/rttys/cache" "github.com/zhaojh329/rttys/config" "github.com/zhaojh329/rttys/utils" ) type credentials struct { Username string `json:"username"` Password string `json:"password"` } var httpSessions *cache.Cache //go:embed frontend/dist var static 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 := sql.Open("sqlite3", 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) if cnt == 0 { return false } return true } func authorizedDev(devid string, cfg *config.Config) bool { if cfg.WhiteList == nil { return true } _, ok := cfg.WhiteList[devid] return ok } func httpAuth(cfg *config.Config, c *gin.Context) bool { if !cfg.LocalAuth { addr, _ := net.ResolveTCPAddr("tcp", c.Request.RemoteAddr) if addr.IP.IsLoopback() { return true } } cookie, err := c.Cookie("sid") if err != nil || !httpSessions.Have(cookie) { return false } httpSessions.Active(cookie, 0) return true } func httpStart(br *broker) { cfg := br.cfg httpSessions = cache.New(30*time.Minute, 5*time.Second) gin.SetMode(gin.ReleaseMode) r := gin.New() authorized := r.Group("/", func(c *gin.Context) { devid := c.Param("devid") if devid != "" && authorizedDev(devid, cfg) { return } if !httpAuth(cfg, c) { c.AbortWithStatus(http.StatusUnauthorized) } }) authorized.GET("/fontsize", func(c *gin.Context) { db, err := sql.Open("sqlite3", 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 := sql.Open("sqlite3", cfg.DB) if err != nil { log.Error().Msg(err.Error()) c.Status(http.StatusInternalServerError) return } defer db.Close() if size < 12 { size = 12 } db.Exec("DELETE FROM config WHERE name = 'FontSize'") db.Exec("INSERT INTO config values('FontSize',?)", fmt.Sprintf("%d", size)) 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"` } devs := make([]DeviceInfo, 0) for id, dev := range br.devices { dev := dev.(*device) devs = append(devs, DeviceInfo{id, uint32(time.Now().Unix() - dev.timestamp), dev.uptime, dev.desc}) } 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/:addr/*path", func(c *gin.Context) { webReqRedirect(br, c) }) r.GET("/authorized/:devid", func(c *gin.Context) { authorized := authorizedDev(c.Param("devid"), cfg) || httpAuth(cfg, c) 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, true, 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 } db, err := sql.Open("sqlite3", cfg.DB) if err != nil { log.Error().Msg(err.Error()) c.Status(http.StatusInternalServerError) return } defer db.Close() _, err = db.Exec("INSERT INTO account values(?,?)", creds.Username, creds.Password) if err != nil { log.Error().Msg(err.Error()) if strings.Contains(err.Error(), "UNIQUE constraint failed") { c.Status(http.StatusForbidden) } else { c.Status(http.StatusInternalServerError) } return } c.Status(http.StatusOK) }) r.NoRoute(func(c *gin.Context) { if !strings.HasPrefix(c.Request.URL.Path, "/frontend/dist/") { c.Request.URL.Path = "/frontend/dist" + c.Request.URL.Path r.HandleContext(c) return } p := path.Clean(c.Request.URL.Path) if p != "/frontend/dist/" { f, err := static.Open(p[1:]) if err != nil { c.Request.URL.Path = "/frontend/dist/" r.HandleContext(c) return } f.Close() } http.FileServer(http.FS(static)).ServeHTTP(c.Writer, c.Request) }) go func() { var err error if cfg.SslCert != "" && cfg.SslKey != "" { log.Info().Msgf("Listen user on: %s SSL on", cfg.AddrUser) err = r.RunTLS(cfg.AddrUser, cfg.SslCert, cfg.SslKey) } else { log.Info().Msgf("Listen user on: %s SSL off", cfg.AddrUser) err = r.Run(cfg.AddrUser) } log.Fatal().Err(err) }() }