From e26337f3eae1501a0758db5a9e0053d2f7b72afe Mon Sep 17 00:00:00 2001 From: Jianhui Zhao Date: Thu, 16 Sep 2021 11:43:30 +0000 Subject: [PATCH] chore: improve code Signed-off-by: Jianhui Zhao --- api.go | 605 +++++++++++++++++++++++++++++++++++++ broker.go | 8 +- config/config.go | 48 +-- device.go | 10 +- http.go | 769 ++++++++++++++--------------------------------- main.go | 12 +- rttys.conf | 4 +- webproxy.go | 302 ------------------- 8 files changed, 879 insertions(+), 879 deletions(-) create mode 100644 api.go delete mode 100644 webproxy.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..06bb15e --- /dev/null +++ b/api.go @@ -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) + }() +} diff --git a/broker.go b/broker.go index 08eff67..1a2c9c4 100644 --- a/broker.go +++ b/broker.go @@ -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) } } } diff --git a/config/config.go b/config/config.go index bf7310d..fa3f831 100644 --- a/config/config.go +++ b/config/config.go @@ -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) diff --git a/device.go b/device.go index 6b81ef3..c65c1d3 100644 --- a/device.go +++ b/device.go @@ -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) diff --git a/http.go b/http.go index 8dd4c35..085d3c4 100644 --- a/http.go +++ b/http.go @@ -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) +} diff --git a/main.go b/main.go index 50f77e1..936cd95 100644 --- a/main.go +++ b/main.go @@ -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", diff --git a/rttys.conf b/rttys.conf index 62f2415..ce7ebca 100644 --- a/rttys.conf +++ b/rttys.conf @@ -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 diff --git a/webproxy.go b/webproxy.go deleted file mode 100644 index 34be18d..0000000 --- a/webproxy.go +++ /dev/null @@ -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) -}