chore: improve code

Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Jianhui Zhao
2021-09-16 11:43:30 +00:00
parent a690e3a54a
commit e26337f3ea
8 changed files with 879 additions and 879 deletions
+605
View File
@@ -0,0 +1,605 @@
package main
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
"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 authorizedDev(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 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()
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 := 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"`
}
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
}
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/:addr/*path", func(c *gin.Context) {
httpProxyRedirect(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, 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
}
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("/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.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.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)
}()
}
+4 -4
View File
@@ -32,7 +32,7 @@ type broker struct {
termMessage chan *termMessage
userMessage chan *usrMessage
cmdMessage chan []byte
webMessage chan *webResp
httpMessage chan *httpResp
devCertPool *x509.CertPool
}
@@ -48,7 +48,7 @@ func newBroker(cfg *config.Config) *broker {
termMessage: make(chan *termMessage, 1000),
userMessage: make(chan *usrMessage, 1000),
cmdMessage: make(chan []byte, 1000),
webMessage: make(chan *webResp, 1000),
httpMessage: make(chan *httpResp, 1000),
}
}
@@ -208,8 +208,8 @@ func (br *broker) run() {
case data := <-br.cmdMessage:
handleCmdResp(data)
case resp := <-br.webMessage:
handleWebResp(resp)
case resp := <-br.httpMessage:
handleHttpProxyResp(resp)
}
}
}
+24 -24
View File
@@ -12,18 +12,18 @@ import (
// Config struct
type Config struct {
AddrDev string
AddrUser string
AddrWeb string
WebRedirURL string
WebPort int
SslCert string
SslKey string
SslCacert string // mTLS for device
Token string
WhiteList map[string]bool
DB string
LocalAuth bool
AddrDev string
AddrUser string
AddrHttpProxy string
HttpProxyRedirURL string
HttpProxyPort int
SslCert string
SslKey string
SslCacert string // mTLS for device
Token string
WhiteList map[string]bool
DB string
LocalAuth bool
}
func getConfigOpt(yamlCfg *yaml.File, name string, opt interface{}) {
@@ -43,16 +43,16 @@ func getConfigOpt(yamlCfg *yaml.File, name string, opt interface{}) {
// Parse config
func Parse(c *cli.Context) *Config {
cfg := &Config{
AddrDev: c.String("addr-dev"),
AddrUser: c.String("addr-user"),
AddrWeb: c.String("addr-web"),
WebRedirURL: c.String("web-redir-url"),
SslCert: c.String("ssl-cert"),
SslKey: c.String("ssl-key"),
SslCacert: c.String("ssl-cacert"),
Token: c.String("token"),
DB: c.String("db"),
LocalAuth: c.Bool("local-auth"),
AddrDev: c.String("addr-dev"),
AddrUser: c.String("addr-user"),
AddrHttpProxy: c.String("addr-http-proxy"),
HttpProxyRedirURL: c.String("http-proxy-redir-url"),
SslCert: c.String("ssl-cert"),
SslKey: c.String("ssl-key"),
SslCacert: c.String("ssl-cacert"),
Token: c.String("token"),
DB: c.String("db"),
LocalAuth: c.Bool("local-auth"),
}
cfg.WhiteList = make(map[string]bool)
@@ -71,8 +71,8 @@ func Parse(c *cli.Context) *Config {
if err == nil {
getConfigOpt(yamlCfg, "addr-dev", &cfg.AddrDev)
getConfigOpt(yamlCfg, "addr-user", &cfg.AddrUser)
getConfigOpt(yamlCfg, "addr-web", &cfg.AddrWeb)
getConfigOpt(yamlCfg, "web-redir-url", &cfg.WebRedirURL)
getConfigOpt(yamlCfg, "addr-http-proxy", &cfg.AddrHttpProxy)
getConfigOpt(yamlCfg, "http-proxy-redir-url", &cfg.HttpProxyRedirURL)
getConfigOpt(yamlCfg, "ssl-cert", &cfg.SslCert)
getConfigOpt(yamlCfg, "ssl-key", &cfg.SslKey)
getConfigOpt(yamlCfg, "ssl-cacert", &cfg.SslCacert)
+5 -5
View File
@@ -27,8 +27,8 @@ const (
msgTypeCmd
msgTypeHeartbeat
msgTypeFile
msgTypeWeb
msgTypeMax = msgTypeWeb
msgTypeHttp
msgTypeMax = msgTypeHttp
)
const rttyProto uint8 = 3
@@ -268,13 +268,13 @@ func (dev *device) readLoop() {
dev.br.cmdMessage <- b
case msgTypeWeb:
case msgTypeHttp:
if msgLen < 18 {
log.Error().Msg("msgTypeWeb: invalid")
log.Error().Msg("msgTypeHttp: invalid")
return
}
dev.br.webMessage <- &webResp{b, dev}
dev.br.httpMessage <- &httpResp{b, dev}
case msgTypeHeartbeat:
parseHeartbeat(dev, b)
+233 -536
View File
@@ -1,605 +1,302 @@
package main
import (
"database/sql"
"embed"
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"rttys/cache"
"rttys/config"
"rttys/client"
"rttys/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type credentials struct {
Username string `json:"username"`
Password string `json:"password"`
type httpProxySession struct {
ctx context.Context
cancel context.CancelFunc
}
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")
type httpResp struct {
data []byte
dev client.Client
}
func httpLogin(cfg *config.Config, creds *credentials) bool {
if creds.Username == "" || creds.Password == "" {
return false
var httpProxyCons sync.Map
var httpProxySessions *cache.Cache
func handleHttpProxyResp(resp *httpResp) {
data := resp.data
addr := data[:18]
data = data[18:]
if len(data) == 0 {
return
}
db, err := instanceDB(cfg.DB)
if cons, ok := httpProxyCons.Load(resp.dev.DeviceID()); ok {
if c, ok := cons.(*sync.Map).Load(string(addr)); ok {
c.(net.Conn).Write(data)
}
}
}
func genDestAddr(addr string) []byte {
destIP, destPort, err := httpProxyVaildAddr(addr)
if err != nil {
log.Error().Msg(err.Error())
return false
return nil
}
defer db.Close()
cnt := 0
b := make([]byte, 6)
copy(b, destIP)
db.QueryRow("SELECT COUNT(*) FROM account WHERE username = ? AND password = ?", creds.Username, creds.Password).Scan(&cnt)
binary.BigEndian.PutUint16(b[4:], destPort)
return cnt != 0
return b
}
func authorizedDev(devid string, cfg *config.Config) bool {
if cfg.WhiteList == nil {
return true
}
func tcpAddr2Bytes(addr *net.TCPAddr) []byte {
b := make([]byte, 18)
_, ok := cfg.WhiteList[devid]
return ok
binary.BigEndian.PutUint16(b[:2], uint16(addr.Port))
copy(b[2:], addr.IP)
return b
}
func isLocalRequest(c *gin.Context) bool {
addr, _ := net.ResolveTCPAddr("tcp", c.Request.RemoteAddr)
return addr.IP.IsLoopback()
type HttpProxyWriter struct {
destAddr []byte
srcAddr []byte
hostHeaderRewrite string
dev client.Client
}
func httpAuth(cfg *config.Config, c *gin.Context) bool {
if !cfg.LocalAuth && isLocalRequest(c) {
return true
}
func (rw *HttpProxyWriter) Write(p []byte) (n int, err error) {
msg := append([]byte{}, rw.srcAddr...)
msg = append(msg, rw.destAddr...)
msg = append(msg, p...)
cookie, err := c.Cookie("sid")
if err != nil || !httpSessions.Have(cookie) {
return false
}
dev := rw.dev.(*device)
httpSessions.Active(cookie, 0)
dev.WriteMsg(msgTypeHttp, msg)
return true
return len(p), nil
}
func isAdminUsername(cfg *config.Config, username string) bool {
if username == "" {
return false
}
func (rw *HttpProxyWriter) WriteRequest(req *http.Request) {
req.Host = rw.hostHeaderRewrite
req.Write(rw)
}
db, err := instanceDB(cfg.DB)
func doHttpProxy(brk *broker, c net.Conn) {
defer c.Close()
br := bufio.NewReader(c)
req, err := http.ReadRequest(br)
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
}
return isAdmin
}
func getLoginUsername(c *gin.Context) string {
cookie, err := c.Cookie("sid")
cookie, err := req.Cookie("rtty-http-devid")
if err != nil {
return ""
return
}
devid := cookie.Value
dev, ok := brk.devices[devid]
if !ok {
return
}
username, ok := httpSessions.Get(cookie)
if ok {
return username.(string)
cookie, err = req.Cookie("rtty-http-sid")
if err != nil {
return
}
sid := cookie.Value
var ctx context.Context
var cancel context.CancelFunc
if v, ok := httpProxySessions.Get(sid); ok {
httpProxySessions.Active(sid, 0)
ctx, cancel = context.WithCancel(v.(*httpProxySession).ctx)
} else {
return
}
return ""
hostHeaderRewrite := "localhost"
cookie, err = req.Cookie("rtty-http-destaddr")
if err == nil {
hostHeaderRewrite, _ = url.QueryUnescape(cookie.Value)
}
destAddr := genDestAddr(hostHeaderRewrite)
srcAddr := tcpAddr2Bytes(c.RemoteAddr().(*net.TCPAddr))
if cons, _ := httpProxyCons.LoadOrStore(devid, &sync.Map{}); true {
cons := cons.(*sync.Map)
cons.Store(string(srcAddr), c)
}
hpw := &HttpProxyWriter{destAddr, srcAddr, hostHeaderRewrite, dev}
req.Host = hostHeaderRewrite
hpw.WriteRequest(req)
go func() {
<-ctx.Done()
// needed, for canceled by new proxy in the same web browser
c.Close()
}()
defer func() {
cons, ok := httpProxyCons.Load(devid)
if ok {
cons := cons.(*sync.Map)
cons.Delete(string(srcAddr))
}
cancel()
}()
for {
req, err := http.ReadRequest(br)
if err != nil {
return
}
httpProxySessions.Active(sid, 0)
hpw.WriteRequest(req)
}
}
func httpStart(br *broker) {
cfg := br.cfg
func listenHttpProxy(brk *broker) {
cfg := brk.cfg
httpSessions = cache.New(30*time.Minute, 5*time.Second)
httpProxySessions = cache.New(10*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 := instanceDB(cfg.DB)
if cfg.AddrHttpProxy != "" {
addr, err := net.ResolveTCPAddr("tcp", cfg.AddrHttpProxy)
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"`
}
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
}
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/: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, 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)
log.Warn().Msg("invalid http proxy addr: " + err.Error())
} else {
c.Status(http.StatusOK)
cfg.HttpProxyPort = addr.Port
}
})
}
r.GET("/signout", func(c *gin.Context) {
cookie, err := c.Cookie("sid")
if err != nil || !httpSessions.Have(cookie) {
return
}
if cfg.HttpProxyPort == 0 {
log.Info().Msg("Automatically select an available port for http proxy")
}
httpSessions.Del(cookie)
ln, err := net.Listen("tcp", cfg.AddrHttpProxy)
if err != nil {
log.Fatal().Msg(err.Error())
}
c.Status(http.StatusOK)
})
cfg.HttpProxyPort = ln.Addr().(*net.TCPAddr).Port
r.POST("/signup", func(c *gin.Context) {
var creds credentials
log.Info().Msgf("Listen http proxy on: %s", ln.Addr().(*net.TCPAddr))
err := c.BindJSON(&creds)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
go func() {
defer ln.Close()
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("/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)
for {
c, err := ln.Accept()
if err != nil {
log.Error().Msg(err.Error())
break
}
if isAdmin && username == loginUsername {
continue
}
users = append(users, username)
go doHttpProxy(brk, c)
}
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.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.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)
}()
}
func httpProxyVaildAddr(addr string) (net.IP, uint16, error) {
ips, ports, err := net.SplitHostPort(addr)
if err != nil {
ips = addr
ports = "80"
}
ip := net.ParseIP(ips)
if ip == nil {
return nil, 0, errors.New("invalid IPv4 Addr")
}
ip = ip.To4()
if ip == nil {
return nil, 0, errors.New("invalid IPv4 Addr")
}
port, _ := strconv.Atoi(ports)
return ip, uint16(port), nil
}
func httpProxyRedirect(br *broker, c *gin.Context) {
cfg := br.cfg
devid := c.Param("devid")
addr := c.Param("addr")
path := c.Param("path")
_, _, err := httpProxyVaildAddr(addr)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
_, ok := br.devices[devid]
if !ok {
c.Status(http.StatusNotFound)
return
}
location := cfg.HttpProxyRedirURL
if location == "" {
host, _, err := net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
location = "http://" + host
if cfg.HttpProxyPort != 80 {
location += fmt.Sprintf(":%d", cfg.HttpProxyPort)
}
}
location += path
location += fmt.Sprintf("?_=%d", time.Now().Unix())
sid, err := c.Cookie("rtty-http-sid")
if err == nil {
if v, ok := httpProxySessions.Get(sid); ok {
v.(*httpProxySession).cancel()
httpProxySessions.Del(sid)
}
}
sid = utils.GenUniqueID("http-proxy")
ctx, cancel := context.WithCancel(context.Background())
httpProxySessions.Set(sid, &httpProxySession{ctx, cancel}, 0)
c.SetCookie("rtty-http-sid", sid, 0, "", "", false, true)
c.SetCookie("rtty-http-devid", devid, 0, "", "", false, true)
c.SetCookie("rtty-http-destaddr", addr, 0, "", "", false, true)
c.Redirect(http.StatusFound, location)
}
+6 -6
View File
@@ -70,8 +70,8 @@ func runRttys(c *cli.Context) {
go br.run()
listenDevice(br)
listenDeviceWeb(br)
httpStart(br)
listenHttpProxy(br)
apiStart(br)
select {}
}
@@ -113,14 +113,14 @@ func main() {
Usage: "address to listen user",
},
&cli.StringFlag{
Name: "addr-web",
Name: "addr-http-proxy",
Value: "",
Usage: "address to listen for web proxy (default auto)",
Usage: "address to listen for HTTP proxy (default auto)",
},
&cli.StringFlag{
Name: "web-redir-url",
Name: "http-proxy-redir-url",
Value: "",
Usage: "url to redirect for access device's web",
Usage: "url to redirect for HTTP proxy",
},
&cli.StringFlag{
Name: "ssl-cert",
+2 -2
View File
@@ -2,9 +2,9 @@
#addr-user: :5913
# Automatically select an available port in default
#addr-web:
#addr-http-proxy:
#web-redir-url:
#http-proxy-redir-url:
#ssl-cacert: /etc/rttys/rttys.ca
#ssl-cert: /etc/rttys/rttys.crt
-302
View File
@@ -1,302 +0,0 @@
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"rttys/cache"
"rttys/client"
"rttys/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type webSession struct {
ctx context.Context
cancel context.CancelFunc
}
type webResp struct {
data []byte
dev client.Client
}
var webCons sync.Map
var webSessions *cache.Cache
func handleWebResp(resp *webResp) {
data := resp.data
addr := data[:18]
data = data[18:]
if len(data) == 0 {
return
}
if cons, ok := webCons.Load(resp.dev.DeviceID()); ok {
if c, ok := cons.(*sync.Map).Load(string(addr)); ok {
c.(net.Conn).Write(data)
}
}
}
func genDestAddr(addr string) []byte {
destIP, destPort, err := webReqVaildAddr(addr)
if err != nil {
return nil
}
b := make([]byte, 6)
copy(b, destIP)
binary.BigEndian.PutUint16(b[4:], destPort)
return b
}
func tcpAddr2Bytes(addr *net.TCPAddr) []byte {
b := make([]byte, 18)
binary.BigEndian.PutUint16(b[:2], uint16(addr.Port))
copy(b[2:], addr.IP)
return b
}
type RttyWebWriter struct {
destAddr []byte
srcAddr []byte
hostHeaderRewrite string
dev client.Client
}
func (rw *RttyWebWriter) Write(p []byte) (n int, err error) {
msg := append([]byte{}, rw.srcAddr...)
msg = append(msg, rw.destAddr...)
msg = append(msg, p...)
dev := rw.dev.(*device)
dev.WriteMsg(msgTypeWeb, msg)
return len(p), nil
}
func (rw *RttyWebWriter) WriteRequest(req *http.Request) {
req.Host = rw.hostHeaderRewrite
req.Write(rw)
}
func webProxy(brk *broker, c net.Conn) {
defer c.Close()
br := bufio.NewReader(c)
req, err := http.ReadRequest(br)
if err != nil {
return
}
cookie, err := req.Cookie("rtty-web-devid")
if err != nil {
return
}
devid := cookie.Value
dev, ok := brk.devices[devid]
if !ok {
return
}
cookie, err = req.Cookie("rtty-web-sid")
if err != nil {
return
}
sid := cookie.Value
var ctx context.Context
var cancel context.CancelFunc
if v, ok := webSessions.Get(sid); ok {
webSessions.Active(sid, 0)
ctx, cancel = context.WithCancel(v.(*webSession).ctx)
} else {
return
}
hostHeaderRewrite := "localhost"
cookie, err = req.Cookie("rtty-web-destaddr")
if err == nil {
hostHeaderRewrite, _ = url.QueryUnescape(cookie.Value)
}
destAddr := genDestAddr(hostHeaderRewrite)
srcAddr := tcpAddr2Bytes(c.RemoteAddr().(*net.TCPAddr))
if cons, _ := webCons.LoadOrStore(devid, &sync.Map{}); true {
cons := cons.(*sync.Map)
cons.Store(string(srcAddr), c)
}
rw := &RttyWebWriter{destAddr, srcAddr, hostHeaderRewrite, dev}
req.Host = hostHeaderRewrite
rw.WriteRequest(req)
go func() {
<-ctx.Done()
// needed, for canceled by new proxy in the same web browser
c.Close()
}()
defer func() {
cons, ok := webCons.Load(devid)
if ok {
cons := cons.(*sync.Map)
cons.Delete(string(srcAddr))
}
cancel()
}()
for {
req, err := http.ReadRequest(br)
if err != nil {
return
}
webSessions.Active(sid, 0)
rw.WriteRequest(req)
}
}
func listenDeviceWeb(brk *broker) {
cfg := brk.cfg
webSessions = cache.New(10*time.Minute, 5*time.Second)
if cfg.AddrWeb != "" {
addr, err := net.ResolveTCPAddr("tcp", cfg.AddrWeb)
if err != nil {
log.Warn().Msg("invalid web proxy addr: " + err.Error())
} else {
cfg.WebPort = addr.Port
}
}
if cfg.WebPort == 0 {
log.Info().Msg("Automatically select an available port for web proxy")
}
ln, err := net.Listen("tcp", cfg.AddrWeb)
if err != nil {
log.Fatal().Msg(err.Error())
}
cfg.WebPort = ln.Addr().(*net.TCPAddr).Port
log.Info().Msgf("Listen web proxy on: %s", ln.Addr().(*net.TCPAddr))
go func() {
defer ln.Close()
for {
c, err := ln.Accept()
if err != nil {
log.Error().Msg(err.Error())
continue
}
go webProxy(brk, c)
}
}()
}
func webReqVaildAddr(addr string) (net.IP, uint16, error) {
ips, ports, err := net.SplitHostPort(addr)
if err != nil {
ips = addr
ports = "80"
}
ip := net.ParseIP(ips)
if ip == nil {
return nil, 0, errors.New("invalid IPv4 Addr")
}
ip = ip.To4()
if ip == nil {
return nil, 0, errors.New("invalid IPv4 Addr")
}
port, _ := strconv.Atoi(ports)
return ip, uint16(port), nil
}
func webReqRedirect(br *broker, c *gin.Context) {
cfg := br.cfg
devid := c.Param("devid")
addr := c.Param("addr")
path := c.Param("path")
_, _, err := webReqVaildAddr(addr)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
_, ok := br.devices[devid]
if !ok {
c.Status(http.StatusNotFound)
return
}
location := cfg.WebRedirURL
if location == "" {
host, _, err := net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
location = "http://" + host
if cfg.WebPort != 80 {
location += fmt.Sprintf(":%d", cfg.WebPort)
}
}
location += path
location += fmt.Sprintf("?_=%d", time.Now().Unix())
sid, err := c.Cookie("rtty-web-sid")
if err == nil {
if v, ok := webSessions.Get(sid); ok {
v.(*webSession).cancel()
webSessions.Del(sid)
}
}
sid = utils.GenUniqueID("web")
ctx, cancel := context.WithCancel(context.Background())
webSessions.Set(sid, &webSession{ctx, cancel}, 0)
c.SetCookie("rtty-web-sid", sid, 0, "", "", false, true)
c.SetCookie("rtty-web-devid", devid, 0, "", "", false, true)
c.SetCookie("rtty-web-destaddr", addr, 0, "", "", false, true)
c.Redirect(http.StatusFound, location)
}