Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Jianhui Zhao
2021-01-22 21:40:05 +08:00
parent 319a21e25d
commit 31d7e0920a
21 changed files with 818 additions and 761 deletions
+6 -30
View File
@@ -29,40 +29,16 @@ This is the server program of [rtty](https://github.com/zhaojh329/rtty)
go get github.com/rakyll/statik
statik -src=frontend/dist
## Command Line Parameters
./rttys -h
Usage of rttys:
-addr-dev string
address to listen device (default ":5912")
-addr-user string
address to listen user (default ":5913")
-conf string
config file to load (default "./rttys.conf")
-gen-token
generate token
-http-password string
password for http auth
-http-username string
username for http auth
-log string
log file path (default "/var/log/rttys.log")
-ssl-cert string
certFile Path
-ssl-key string
keyFile Path
-token string
token to use
-white-list string
white list(device IDs separated by spaces or *)
## Authorization
Generate a token
./rttys -gen-token
$ rttys token
Please set a password:******
Your token is: 34762d07637276694b938d23f10d7164
./rttys -token 34762d07637276694b938d23f10d7164
Use token
$ rttys run -t 34762d07637276694b938d23f10d7164
## Running as a Linux service
Move the rttys binary into /usr/local/bin/
@@ -81,7 +57,7 @@ Create a systemd unit file: /etc/systemd/system/rttys.service
After=network.target
[Service]
ExecStart=/usr/local/bin/rttys -conf /etc/rttys/rttys.conf
ExecStart=/usr/local/bin/rttys run -c /etc/rttys/rttys.conf
TimeoutStopSec=5s
[Install]
+6 -30
View File
@@ -29,40 +29,16 @@
go get github.com/rakyll/statik
statik -src=frontend/dist
## 命令行参数
./rttys -h
Usage of rttys:
-addr-dev string
address to listen device (default ":5912")
-addr-user string
address to listen user (default ":5913")
-conf string
config file to load (default "./rttys.conf")
-gen-token
generate token
-http-password string
password for http auth
-http-username string
username for http auth
-log string
log file path (default "/var/log/rttys.log")
-ssl-cert string
certFile Path
-ssl-key string
keyFile Path
-token string
token to use
-white-list string
white list(device IDs separated by spaces or *)
## 认证
生成一个 token
./rttys -gen-token
$ rttys token
Please set a password:******
Your token is: 34762d07637276694b938d23f10d7164
./rttys -token 34762d07637276694b938d23f10d7164
使用 token
$rttys -t 34762d07637276694b938d23f10d7164
## 作为Linux服务运行
移动rttys可执行程序到/usr/local/bin/
@@ -81,7 +57,7 @@
After=network.target
[Service]
ExecStart=/usr/local/bin/rttys -conf /etc/rttys/rttys.conf
ExecStart=/usr/local/bin/rttys run -c /etc/rttys/rttys.conf
TimeoutStopSec=5s
[Install]
+140 -116
View File
@@ -6,163 +6,193 @@ import (
"github.com/gorilla/websocket"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/zhaojh329/rttys/client"
"github.com/zhaojh329/rttys/config"
)
type session struct {
dev *device
u *user
devid string
devsid byte
u client.Client
}
type broker struct {
cfg *rttysConfig
login chan *user
logout chan string
register chan *device
unregister chan *device
devices map[string]*device
sessions map[string]*session
commands map[string]*commandStatus
newSession chan *session
cmdReq chan *commandReq
webCon chan *webNewCon
webReq chan *webReq
devMessage chan *devMessage
userMessage chan *usrMessage
cmdMessage chan []byte
webMessage chan *webResp
clearCmd chan string
cfg *config.Config
devices map[string]client.Client
loginAck chan *loginAckMsg
logout chan string
register chan client.Client
unregister chan client.Client
waitLoginUsers map[string]client.Client
sessions map[string]*session
cmdReq chan *commandReq
webCon chan *webNewCon
webReq chan *webReq
termMessage chan *termMessage
userMessage chan *usrMessage
cmdMessage chan []byte
webMessage chan *webResp
}
func newBroker(cfg *rttysConfig) *broker {
func newBroker(cfg *config.Config) *broker {
return &broker{
cfg: cfg,
login: make(chan *user, 10),
logout: make(chan string, 10),
register: make(chan *device, 1000),
unregister: make(chan *device, 1000),
devices: make(map[string]*device),
sessions: make(map[string]*session),
newSession: make(chan *session, 10),
commands: make(map[string]*commandStatus),
cmdReq: make(chan *commandReq, 1000),
webCon: make(chan *webNewCon, 1000),
webReq: make(chan *webReq, 1000),
devMessage: make(chan *devMessage, 1000),
userMessage: make(chan *usrMessage, 1000),
cmdMessage: make(chan []byte, 1000),
webMessage: make(chan *webResp, 1000),
clearCmd: make(chan string, 1000),
cfg: cfg,
loginAck: make(chan *loginAckMsg, 1000),
logout: make(chan string, 1000),
register: make(chan client.Client, 1000),
unregister: make(chan client.Client, 1000),
devices: make(map[string]client.Client),
waitLoginUsers: make(map[string]client.Client),
sessions: make(map[string]*session),
cmdReq: make(chan *commandReq, 1000),
webCon: make(chan *webNewCon, 1000),
webReq: make(chan *webReq, 1000),
termMessage: make(chan *termMessage, 1000),
userMessage: make(chan *usrMessage, 1000),
cmdMessage: make(chan []byte, 1000),
webMessage: make(chan *webResp, 1000),
}
}
func (br *broker) run() {
for {
select {
case dev := <-br.register:
err := byte(0)
msg := "OK"
case c := <-br.register:
devid := c.DeviceID()
if _, ok := br.devices[dev.id]; ok {
log.Error().Msg("Device ID conflicting: " + dev.id)
msg = "ID conflicting"
err = 1
} else if dev.token != br.cfg.token {
log.Error().Msg("Invalid token from terminal device")
msg = "Invalid token"
err = 1
if c.IsDevice() {
err := byte(0)
msg := "OK"
if _, ok := br.devices[devid]; ok {
log.Error().Msg("Device ID conflicting: " + devid)
msg = "ID conflicting"
err = 1
} else if c.(*device).token != br.cfg.Token {
log.Error().Msg("Invalid token from terminal device")
msg = "Invalid token"
err = 1
} else {
br.devices[devid] = c
log.Info().Msg("New device: " + devid)
}
c.WriteMsg(msgTypeRegister, append([]byte{err}, msg...))
} else {
br.devices[dev.id] = dev
log.Info().Msg("New device: " + dev.id)
if dev, ok := br.devices[devid]; ok {
if _, ok := br.waitLoginUsers[devid]; ok {
userLoginAck(loginErrorBusy, c)
log.Error().Msg("login fail, device busy")
} else {
br.waitLoginUsers[devid] = c
dev.WriteMsg(msgTypeLogin, []byte{})
}
} else {
userLoginAck(loginErrorOffline, c)
log.Error().Msgf("Not found the device '%s'", devid)
}
}
dev.writeMsg(msgTypeRegister, append([]byte{err}, msg...))
if err == 1 {
dev.close()
}
case c := <-br.unregister:
id := c.DeviceID()
case dev := <-br.unregister:
if _, ok := br.devices[dev.id]; ok {
delete(br.devices, dev.id)
}
if c.IsDevice() {
if _, ok := br.devices[id]; ok {
delete(br.devices, id)
}
for sid, session := range br.sessions {
if session.dev == dev {
session.u.close()
for sid, s := range br.sessions {
if s.devid == id {
s.u.Close()
delete(br.sessions, sid)
log.Info().Msg("Delete session: " + sid)
}
}
} else {
sid := c.(*user).sid
if s, ok := br.sessions[sid]; ok {
delete(br.sessions, sid)
c.Close()
if dev, ok := br.devices[s.devid]; ok {
dev.WriteMsg(msgTypeLogout, []byte{sid[len(sid)-1] - '0'})
}
log.Info().Msg("Delete session: " + sid)
}
}
case u := <-br.login:
if dev, ok := br.devices[u.devid]; ok {
if !dev.login(u) {
u.loginAck(loginErrorBusy)
log.Error().Msgf("Device '%s' is busy", dev.id)
case msg := <-br.loginAck:
if c, ok := br.waitLoginUsers[msg.devid]; ok {
if msg.isBusy {
userLoginAck(loginErrorBusy, c)
log.Error().Msg("login fail, device busy")
} else {
sid := msg.devid + string(msg.sid+'0')
br.sessions[sid] = &session{msg.devid, msg.sid, c}
u := c.(*user)
u.sid = sid
userLoginAck(loginErrorNone, c)
log.Info().Msg("New session: " + sid)
}
} else {
u.loginAck(loginErrorOffline)
log.Error().Msgf("Not found the device '%s'", u.devid)
delete(br.waitLoginUsers, msg.devid)
}
// device active logout
// typically, executing the exit command at the terminal will case this
case sid := <-br.logout:
if session, ok := br.sessions[sid]; ok {
if s, ok := br.sessions[sid]; ok {
delete(br.sessions, sid)
session.u.close()
session.dev.logout(sid[len(sid)-1] - '0')
s.u.Close()
log.Info().Msg("Delete session: " + sid)
}
case session := <-br.newSession:
sid := session.dev.id + string(session.devsid+'0')
session.u.sid = sid
session.u.loginAck(loginErrorNone)
br.sessions[sid] = session
log.Info().Msg("New session: " + sid)
case msg := <-br.devMessage:
sid := msg.devid + string(msg.sid+'0')
if session, ok := br.sessions[sid]; ok {
data := []byte{0}
if msg.isFileMsg {
data[0] = 1
}
session.u.writeMessage(websocket.BinaryMessage, append(data, msg.data...))
// from device, includes terminal data and file data
case msg := <-br.termMessage:
if s, ok := br.sessions[msg.sid]; ok {
s.u.WriteMsg(websocket.BinaryMessage, msg.data)
}
case msg := <-br.userMessage:
msgType := msg.msgType
data := msg.data
if session, ok := br.sessions[msg.sid]; ok {
devsid := msg.sid[len(msg.sid)-1] - '0'
if msgType == websocket.BinaryMessage {
isFileMsg := data[0] == 1
data = data[1:]
if isFileMsg {
session.dev.writeMsg(msgTypeFile, data)
if s, ok := br.sessions[msg.sid]; ok {
if dev, ok := br.devices[s.devid]; ok {
devsid := msg.sid[len(msg.sid)-1] - '0'
data := msg.data
if msg.typ == websocket.BinaryMessage {
if data[0] == 1 {
dev.WriteMsg(msgTypeFile, data[1:])
} else {
dev.WriteMsg(msgTypeTermData, append([]byte{devsid}, data[1:]...))
}
} else {
session.dev.writeMsg(msgTypeTermData, append([]byte{devsid}, data...))
}
} else {
typ := jsoniter.Get(msg.data, "type").ToString()
switch typ {
case "winsize":
cols := jsoniter.Get(msg.data, "cols").ToUint()
rows := jsoniter.Get(msg.data, "rows").ToUint()
typ := jsoniter.Get(data, "type").ToString()
b := make([]byte, 4)
binary.BigEndian.PutUint16(b, uint16(cols))
binary.BigEndian.PutUint16(b[2:], uint16(rows))
switch typ {
case "winsize":
b := [5]byte{devsid}
session.dev.writeMsg(msgTypeWinsize, append([]byte{devsid}, b...))
cols := jsoniter.Get(data, "cols").ToUint()
rows := jsoniter.Get(data, "rows").ToUint()
binary.BigEndian.PutUint16(b[1:], uint16(cols))
binary.BigEndian.PutUint16(b[3:], uint16(rows))
dev.WriteMsg(msgTypeWinsize, b[:])
}
}
}
} else {
log.Error().Msg("Not found sid: " + msg.sid)
}
case cmdReq := <-br.cmdReq:
handleCmdReq(br, cmdReq)
case req := <-br.cmdReq:
req.dev.WriteMsg(msgTypeCmd, req.data)
case c := <-br.webCon:
handleWebCon(br, c)
@@ -175,12 +205,6 @@ func (br *broker) run() {
case resp := <-br.webMessage:
handleWebResp(resp)
case token := <-br.clearCmd:
if cmd, ok := br.commands[token]; ok {
delete(br.commands, token)
cmd.tmr.Stop()
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
package client
// Client abstract device and user
type Client interface {
WriteMsg(typ int, data []byte)
// For users, return the device ID that the user wants to access
// For devices, return the ID of the device
DeviceID() string
IsDevice() bool
Close()
}
+88 -85
View File
@@ -1,39 +1,33 @@
package main
import (
"fmt"
"io"
"context"
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/zhaojh329/rttys/client"
"github.com/zhaojh329/rttys/utils"
jsoniter "github.com/json-iterator/go"
)
const commandTimeout = time.Second * 30
const commandTimeout = 30 // second
const (
rttyCmdErrInvalid = 1001
rttyCmdErrOffline = 1002
rttyCmdErrBusy = 1003
rttyCmdErrTimeout = 1004
rttyCmdErrPending = 1005
rttyCmdErrInvalidToken = 1006
rttyCmdErrInvalid = 1001
rttyCmdErrOffline = 1002
rttyCmdErrTimeout = 1003
)
var cmdErrMsg = map[int]string{
rttyCmdErrInvalid: "invalid format",
rttyCmdErrOffline: "device offline",
rttyCmdErrBusy: "server is busy",
rttyCmdErrTimeout: "timeout",
rttyCmdErrPending: "pending",
rttyCmdErrInvalidToken: "invalid token",
}
type commandStatus struct {
ts time.Time
token string
resp string
tmr *time.Timer
rttyCmdErrInvalid: "invalid format",
rttyCmdErrOffline: "device offline",
rttyCmdErrTimeout: "timeout",
}
type commandInfo struct {
@@ -43,99 +37,108 @@ type commandInfo struct {
}
type commandReq struct {
done chan struct{}
token string
content []byte
devid string
w http.ResponseWriter
cancel context.CancelFunc
dev client.Client
c *gin.Context
data []byte
}
var commands sync.Map
func handleCmdResp(br *broker, data []byte) {
token := jsoniter.Get(data, "token").ToString()
if cmd, ok := br.commands[token]; ok {
cmd.resp = jsoniter.Get(data, "attrs").ToString()
if req, ok := commands.Load(token); ok {
req := req.(*commandReq)
req.c.String(http.StatusOK, jsoniter.Get(data, "attrs").ToString())
req.cancel()
}
}
func cmdErrReply(err int, req *commandReq) {
fmt.Fprintf(req.w, `{"err": %d, "msg":"%s"}`, err, cmdErrMsg[err])
close(req.done)
req.c.JSON(http.StatusOK, gin.H{
"err": err,
"msg": cmdErrMsg[err],
})
req.cancel()
}
func handleCmdReq(br *broker, req *commandReq) {
token := req.token
func handleCmdReq(br *broker, c *gin.Context) {
devid := c.Param("devid")
if token != "" {
if cmd, ok := br.commands[token]; ok {
if len(cmd.resp) == 0 {
if time.Now().Sub(cmd.ts) > commandTimeout {
cmdErrReply(rttyCmdErrTimeout, req)
} else {
cmdErrReply(rttyCmdErrPending, req)
}
} else {
io.WriteString(req.w, cmd.resp)
close(req.done)
br.clearCmd <- token
}
} else {
cmdErrReply(rttyCmdErrInvalidToken, req)
}
return
ctx, cancel := context.WithCancel(context.Background())
req := &commandReq{
cancel: cancel,
c: c,
}
cmdInfo := commandInfo{}
err := jsoniter.Unmarshal(req.content, &cmdInfo)
if err != nil || cmdInfo.Cmd == "" {
cmdErrReply(rttyCmdErrInvalid, req)
return
}
dev, ok := br.devices[req.devid]
dev, ok := br.devices[devid]
if !ok {
cmdErrReply(rttyCmdErrOffline, req)
return
}
token = genUniqueID("cmd")
req.dev = dev
cmd := &commandStatus{
ts: time.Now(),
token: token,
tmr: time.AfterFunc(commandTimeout+time.Second*2, func() {
br.clearCmd <- token
}),
content, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
br.commands[token] = cmd
cmdInfo := commandInfo{}
err = jsoniter.Unmarshal(content, &cmdInfo)
if err != nil || cmdInfo.Cmd == "" {
cmdErrReply(rttyCmdErrInvalid, req)
return
}
username := jsoniter.Get(req.content, "username").ToString()
password := jsoniter.Get(req.content, "password").ToString()
cmdName := jsoniter.Get(req.content, "cmd").ToString()
params := jsoniter.Get(req.content, "params")
token := utils.GenUniqueID("cmd")
var data []byte
data = append(data, username...)
data = append(data, 0)
params := jsoniter.Get(content, "params")
data = append(data, password...)
data = append(data, 0)
data := make([]string, 5)
data = append(data, cmdName...)
data = append(data, 0)
data[0] = jsoniter.Get(content, "username").ToString()
data[1] = jsoniter.Get(content, "password").ToString()
data[2] = jsoniter.Get(content, "cmd").ToString()
data[3] = token
data[4] = string(byte(params.Size()))
data = append(data, token...)
data = append(data, 0)
req.data = []byte(strings.Join(data, string(byte(0))))
data = append(data, byte(params.Size()))
for i := 0; i < params.Size(); i++ {
data = append(data, params.Get(i).ToString()...)
data = append(data, 0)
req.data = append(req.data, params.Get(i).ToString()...)
req.data = append(req.data, 0)
}
dev.writeMsg(msgTypeCmd, data)
br.cmdReq <- req
fmt.Fprintf(req.w, `{"token":"%s"}`, token)
close(req.done)
waitTime := commandTimeout
wait := c.Query("wait")
if wait != "" {
waitTime, _ = strconv.Atoi(wait)
}
if waitTime == 0 {
c.Status(http.StatusOK)
return
}
commands.Store(token, req)
if waitTime < 0 || waitTime > commandTimeout {
waitTime = commandTimeout
}
tmr := time.NewTimer(time.Second * time.Duration(waitTime))
select {
case <-tmr.C:
cmdErrReply(rttyCmdErrTimeout, req)
commands.Delete(token)
case <-ctx.Done():
}
}
-124
View File
@@ -1,124 +0,0 @@
package main
import (
"flag"
"os"
"strconv"
"strings"
"github.com/kylelemons/go-gypsy/yaml"
"github.com/rs/zerolog/log"
)
type rttysConfig struct {
addrDev string
addrUser string
addrWeb string
webRedirUrl string
webPort int
sslCert string
sslKey string
httpUsername string
httpPassword string
token string
fontSize int
whiteList map[string]bool
}
func getConfigOpt(yamlCfg *yaml.File, name string, opt interface{}) {
val, err := yamlCfg.Get(name)
if err != nil {
return
}
switch opt := opt.(type) {
case *string:
*opt = val
case *int:
*opt, _ = strconv.Atoi(val)
}
}
func parseConfig() *rttysConfig {
cfg := &rttysConfig{}
cfg.whiteList = make(map[string]bool)
flag.StringVar(&cfg.addrDev, "addr-dev", ":5912", "address to listen device")
flag.StringVar(&cfg.addrUser, "addr-user", ":5913", "address to listen user")
flag.StringVar(&cfg.addrWeb, "addr-web", ":5914", "address to listen for access device's web")
flag.StringVar(&cfg.webRedirUrl, "web-redir-url", "", "url to redirect for access device's web")
flag.StringVar(&cfg.sslCert, "ssl-cert", "", "certFile Path")
flag.StringVar(&cfg.sslKey, "ssl-key", "", "keyFile Path")
flag.StringVar(&cfg.httpUsername, "http-username", "", "username for http auth")
flag.StringVar(&cfg.httpPassword, "http-password", "", "password for http auth")
flag.StringVar(&cfg.token, "token", "", "token to use")
conf := flag.String("conf", "./rttys.conf", "config file to load")
genToken := flag.Bool("gen-token", false, "generate token")
whiteList := flag.String("white-list", "", "white list(device IDs separated by spaces or *)")
if *whiteList == "*" {
cfg.whiteList = nil
} else {
for _, id := range strings.Fields(*whiteList) {
cfg.whiteList[id] = true
}
}
flag.Parse()
if *genToken {
genTokenAndExit()
}
yamlCfg, err := yaml.ReadFile(*conf)
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, "ssl-cert", &cfg.sslCert)
getConfigOpt(yamlCfg, "ssl-key", &cfg.sslKey)
getConfigOpt(yamlCfg, "http-username", &cfg.httpUsername)
getConfigOpt(yamlCfg, "http-password", &cfg.httpPassword)
getConfigOpt(yamlCfg, "token", &cfg.token)
getConfigOpt(yamlCfg, "font-size", &cfg.fontSize)
val, err := yamlCfg.Get("white-list")
if err == nil {
if val == "*" || val == "\"*\"" {
cfg.whiteList = nil
} else {
for _, id := range strings.Fields(val) {
cfg.whiteList[id] = true
}
}
}
}
if cfg.fontSize == 0 {
cfg.fontSize = 16
}
if cfg.fontSize < 12 {
cfg.fontSize = 12
}
if cfg.sslCert != "" && cfg.sslKey != "" {
_, err := os.Lstat(cfg.sslCert)
if err != nil {
log.Error().Msg(err.Error())
cfg.sslCert = ""
}
_, err = os.Lstat(cfg.sslKey)
if err != nil {
log.Error().Msg(err.Error())
cfg.sslKey = ""
}
}
return cfg
}
+117
View File
@@ -0,0 +1,117 @@
package config
import (
"os"
"strconv"
"strings"
"github.com/kylelemons/go-gypsy/yaml"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
)
// Config struct
type Config struct {
AddrDev string
AddrUser string
AddrWeb string
WebRedirURL string
WebPort int
SslCert string
SslKey string
HTTPUsername string
HTTPPassword string
Token string
FontSize int
WhiteList map[string]bool
}
func getConfigOpt(yamlCfg *yaml.File, name string, opt interface{}) {
val, err := yamlCfg.Get(name)
if err != nil {
return
}
switch opt := opt.(type) {
case *string:
*opt = val
case *int:
*opt, _ = strconv.Atoi(val)
}
}
// 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"),
HTTPUsername: c.String("http-username"),
HTTPPassword: c.String("http-password"),
Token: c.String("token"),
}
cfg.WhiteList = make(map[string]bool)
whiteList := c.String("white-list")
if whiteList == "*" {
cfg.WhiteList = nil
} else {
for _, id := range strings.Fields(whiteList) {
cfg.WhiteList[id] = true
}
}
yamlCfg, err := yaml.ReadFile(c.String("conf"))
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, "ssl-cert", &cfg.SslCert)
getConfigOpt(yamlCfg, "ssl-key", &cfg.SslKey)
getConfigOpt(yamlCfg, "http-username", &cfg.HTTPUsername)
getConfigOpt(yamlCfg, "http-password", &cfg.HTTPPassword)
getConfigOpt(yamlCfg, "token", &cfg.Token)
getConfigOpt(yamlCfg, "font-size", &cfg.FontSize)
val, err := yamlCfg.Get("white-list")
if err == nil {
if val == "*" || val == "\"*\"" {
cfg.WhiteList = nil
} else {
for _, id := range strings.Fields(val) {
cfg.WhiteList[id] = true
}
}
}
}
if cfg.FontSize == 0 {
cfg.FontSize = 16
}
if cfg.FontSize < 12 {
cfg.FontSize = 12
}
if cfg.SslCert != "" && cfg.SslKey != "" {
_, err := os.Lstat(cfg.SslCert)
if err != nil {
log.Error().Msg(err.Error())
cfg.SslCert = ""
}
_, err = os.Lstat(cfg.SslKey)
if err != nil {
log.Error().Msg(err.Error())
cfg.SslKey = ""
}
}
return cfg
}
+69 -83
View File
@@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"encoding/binary"
@@ -37,65 +38,36 @@ type device struct {
uptime uint32
token string
conn net.Conn
loginMutex sync.Mutex
u *user /* User who is wait login */
active time.Time
closeMutex sync.Mutex
closed bool
closeCh chan struct{}
cancel context.CancelFunc
}
type devMessage struct {
devid string
sid uint8
data []byte
isFileMsg bool
type termMessage struct {
sid string
data []byte
}
func (dev *device) login(u *user) bool {
defer dev.loginMutex.Unlock()
type loginAckMsg struct {
devid string
sid byte
isBusy bool
}
dev.loginMutex.Lock()
if dev.u != nil {
return false
}
dev.u = u
dev.writeMsg(msgTypeLogin, []byte{})
func (dev *device) IsDevice() bool {
return true
}
func (dev *device) handleLogin(code byte, sid byte) {
defer dev.loginMutex.Unlock()
dev.loginMutex.Lock()
if dev.u == nil {
return
}
u := dev.u
dev.u = nil
if code == 1 {
log.Error().Msg("login fail, device busy")
u.loginAck(loginErrorBusy)
return
}
dev.br.newSession <- &session{dev, u, sid}
func (dev *device) DeviceID() string {
return dev.id
}
func (dev *device) logout(sid byte) {
dev.writeMsg(msgTypeLogout, []byte{sid})
}
func (dev *device) keepAlive() {
func (dev *device) keepAlive(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
ninactive := 0
ninactive := 0
lastHeartbeat := time.Now()
for {
@@ -106,7 +78,7 @@ func (dev *device) keepAlive() {
log.Error().Msgf("Inactive device in long time: %s", dev.id)
if ninactive > 1 {
log.Error().Msgf("Inactive 3 times, now kill it: %s", dev.id)
dev.close()
dev.Close()
return
}
ninactive = ninactive + 1
@@ -114,32 +86,34 @@ func (dev *device) keepAlive() {
if now.Sub(lastHeartbeat) > heartbeatInterval-1 {
lastHeartbeat = now
dev.writeMsg(msgTypeHeartbeat, []byte{})
dev.WriteMsg(msgTypeHeartbeat, []byte{})
}
case <-dev.closeCh:
case <-ctx.Done():
return
}
}
}
func (dev *device) close() {
func (dev *device) Close() {
defer dev.closeMutex.Unlock()
dev.closeMutex.Lock()
if !dev.closed {
dev.closed = true
time.AfterFunc(time.Second, func() {
dev.br.unregister <- dev
close(dev.closeCh)
dev.conn.Close()
log.Info().Msgf("Device '%s' closed", dev.id)
})
dev.conn.Close()
dev.cancel()
dev.br.unregister <- dev
log.Info().Msgf("Device '%s' closed", dev.id)
}
}
func (dev *device) writeMsg(typ byte, data []byte) {
b := []byte{typ, 0, 0}
func (dev *device) WriteMsg(typ int, data []byte) {
b := []byte{byte(typ), 0, 0}
binary.BigEndian.PutUint16(b[1:], uint16(len(data)))
@@ -165,7 +139,7 @@ func parseHeartbeat(dev *device, b []byte) {
}
func (dev *device) readLoop() {
defer dev.close()
defer dev.Close()
br := bufio.NewReader(dev.conn)
@@ -206,17 +180,23 @@ func (dev *device) readLoop() {
if code == 0 {
sid = b[1]
}
dev.handleLogin(code, sid)
dev.br.loginAck <- &loginAckMsg{dev.id, sid, code == 1}
case msgTypeLogout:
sid := b[0]
dev.br.logout <- dev.id + string(sid+'0')
dev.br.logout <- dev.id + string(b[0]+'0')
case msgTypeTermData:
fallthrough
case msgTypeFile:
sid := b[0]
dev.br.devMessage <- &devMessage{dev.id, sid, b[1:], typ == msgTypeFile}
sid := dev.id + string(b[0]+'0')
if typ == msgTypeFile {
b[0] = 1
} else {
b[0] = 0
}
dev.br.termMessage <- &termMessage{sid, b}
case msgTypeCmd:
dev.br.cmdMessage <- b
@@ -226,6 +206,7 @@ func (dev *device) readLoop() {
case msgTypeHeartbeat:
parseHeartbeat(dev, b)
default:
log.Error().Msgf("invalid msg type: %d", typ)
}
@@ -235,14 +216,13 @@ func (dev *device) readLoop() {
func listenDevice(br *broker) {
cfg := br.cfg
ln, err := net.Listen("tcp", cfg.addrDev)
ln, err := net.Listen("tcp", cfg.AddrDev)
if err != nil {
log.Fatal().Msg(err.Error())
}
defer ln.Close()
if cfg.sslCert != "" && cfg.sslKey != "" {
crt, err := tls.LoadX509KeyPair(cfg.sslCert, cfg.sslKey)
if cfg.SslCert != "" && cfg.SslKey != "" {
crt, err := tls.LoadX509KeyPair(cfg.SslCert, cfg.SslKey)
if err != nil {
log.Fatal().Msg(err.Error())
}
@@ -253,27 +233,33 @@ func listenDevice(br *broker) {
tlsConfig.Rand = rand.Reader
ln = tls.NewListener(ln, tlsConfig)
log.Info().Msgf("Listen device on: %s SSL on", cfg.addrDev)
log.Info().Msgf("Listen device on: %s SSL on", cfg.AddrDev)
} else {
log.Info().Msgf("Listen device on: %s SSL off", cfg.addrDev)
log.Info().Msgf("Listen device on: %s SSL off", cfg.AddrDev)
}
for {
conn, err := ln.Accept()
if err != nil {
log.Error().Msg(err.Error())
continue
}
go func() {
defer ln.Close()
dev := &device{
br: br,
conn: conn,
closeCh: make(chan struct{}),
active: time.Now(),
timestamp: time.Now().Unix(),
}
for {
conn, err := ln.Accept()
if err != nil {
log.Error().Msg(err.Error())
continue
}
go dev.readLoop()
go dev.keepAlive()
}
ctx, cancel := context.WithCancel(context.Background())
dev := &device{
br: br,
conn: conn,
cancel: cancel,
active: time.Now(),
timestamp: time.Now().Unix(),
}
go dev.readLoop()
go dev.keepAlive(ctx)
}
}()
}
+2
View File
@@ -22,7 +22,9 @@
"OK": "OK",
"Ignore": "Ignore",
"command is required": "command is required",
"must be an integer between 0 and 30": "must be an integer between 0 and 30",
"Parameter": "Parameter",
"Wait Time": "Wait Time",
"Status of executive command": "Status of executive command",
"cmd-status-total": "Total: {count}",
"cmd-status-fail": "Fail: {count}",
+2
View File
@@ -22,7 +22,9 @@
"OK": "确定",
"Ignore": "忽略",
"command is required": "命令为必填项",
"must be an integer between 0 and 30": "必须是 0 到 30 之间的整数",
"Parameter": "参数",
"Wait Time": "等待时间",
"Status of executive command": "命令执行状态",
"cmd-status-total": "总数: {count}",
"cmd-status-fail": "失败: {count}",
+47 -82
View File
@@ -40,7 +40,7 @@
<el-input v-model="cmdData.password" show-password/>
</el-form-item>
<el-form-item :label="$t('Command')" prop="cmd">
<el-input v-model="cmdData.cmd"/>
<el-input v-model.trim="cmdData.cmd"/>
</el-form-item>
<el-form-item :label="$t('Parameter')" prop="params">
<el-tag :key="tag" v-for="tag in cmdData.params" closable @close="delCmdParam(tag)">{{tag}}</el-tag>
@@ -50,6 +50,11 @@
<el-button v-else style="width: 40px; margin-left: 10px;" size="small" icon="el-icon-plus" type="primary"
@click="showInputParam"/>
</el-form-item>
<el-form-item :label="$t('Wait Time')" prop="wait">
<el-input v-model.number="cmdData.wait" placeholder="30">
<template slot="append">s</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 70%" @click="doCmd">{{ $t('OK') }}</el-button>
<el-button type="warning" @click="resetCmdData">{{ $t('Reset') }}</el-button>
@@ -70,7 +75,6 @@
<el-table :data="cmdStatus.responses" height="300" :empty-text="$t('No Response')">
<el-table-column type="index" label="#"/>
<el-table-column prop="id" :label="$t('Device ID')"/>
<el-table-column prop="cmd" :label="$t('Command')"/>
<el-table-column prop="err" :label="$t('Error Code')"/>
<el-table-column prop="msg" :label="$t('Error Message')" show-overflow-tooltip/>
<el-table-column prop="code" :label="$t('Status Code')"/>
@@ -114,16 +118,8 @@
description: string;
}
interface CmdStatusInfo {
querying: boolean;
devid: string;
cmd: string;
}
interface ResponseInfo {
token: string;
devid: string;
cmd: string;
id: string;
code: number;
err: number;
msg: string;
@@ -177,7 +173,6 @@
modal: false,
execing: 0,
fail: 0,
running: {} as { [key: string]: CmdStatusInfo },
respModal: false,
responses: [] as ResponseInfo[]
};
@@ -186,11 +181,24 @@
password: '',
cmd: '',
params: [] as string[],
currentParam: ''
currentParam: '',
wait: 30
};
cmdRuleValidate = {
username: [{required: true, trigger: 'blur', message: this.tr('username is required')}],
cmd: [{required: true, trigger: 'blur', message: this.tr('command is required')}]
username: [{required: true, message: this.tr('username is required')}],
cmd: [{required: true, message: this.tr('command is required')}],
wait: [{validator: (rule, value, callback) => {
if (!value) {
callback()
return;
}
if (!Number.isInteger(value) || value < 0 || value > 30) {
callback(new Error(this.tr('must be an integer between 0 and 30')));
}
callback()
}}]
};
tr(key: string): string {
@@ -264,50 +272,6 @@
this.cmdModal = true;
}
queryCmdResp() {
let count = 0;
for (const token in this.cmdStatus.running) {
const item = this.cmdStatus.running[token];
if (item.querying)
continue;
item.querying = true;
this.axios.get(`/cmd/${item.devid}/${token}`).then(response => {
const resp = response.data as ResponseInfo;
if (resp.err === 1005) {
item.querying = false;
return;
}
if (resp.err && resp.err !== 0)
this.cmdStatus.fail++;
this.cmdStatus.execing--;
resp.devid = item.devid;
resp.cmd = item.cmd;
resp.stdout = window.atob(resp.stdout || '');
resp.stderr = window.atob(resp.stderr || '');
this.cmdStatus.responses.push(resp);
delete this.cmdStatus.running[token];
});
count++;
if (count > 10)
break;
}
if (this.cmdStatus.execing > 0)
setTimeout(this.queryCmdResp, 500);
}
delCmdParam(tag: string) {
this.cmdData.params.splice(this.cmdData.params.indexOf(tag), 1);
}
@@ -336,41 +300,44 @@
this.cmdStatus.total = this.selection.length;
this.cmdStatus.execing = this.selection.length;
this.cmdStatus.fail = 0;
this.cmdStatus.running = {};
this.cmdStatus.responses = [];
this.selection.forEach(item => {
const data = {
username: this.cmdData.username,
password: this.cmdData.password,
sid: sessionStorage.getItem('rtty-sid'),
cmd: this.cmdData.cmd.trim(),
cmd: this.cmdData.cmd,
params: this.cmdData.params
};
this.axios.post(`/cmd/${item.id}`, data).then((response) => {
const resp = response.data as ResponseInfo;
this.axios.post(`/cmd/${item.id}?wait=${this.cmdData.wait}`, data).then((response) => {
if (this.cmdData.wait === 0) {
this.cmdStatus.responses.push({
err: 0,
msg: '',
id: item.id,
code: 0,
stdout: '',
stderr: ''
});
} else {
const resp = response.data as ResponseInfo;
if (resp.token) {
this.cmdStatus.running[resp.token] = {
devid: item.id,
cmd: data.cmd,
querying: false
};
return;
if (resp.err && resp.err !== 0) {
this.cmdStatus.fail++;
resp.stdout = '';
resp.stderr = '';
} else {
resp.stdout = window.atob(resp.stdout || '');
resp.stderr = window.atob(resp.stderr || '');
}
resp.id = item.id;
this.cmdStatus.responses.push(resp);
}
this.cmdStatus.execing--;
this.cmdStatus.fail++;
resp.devid = item.id;
resp.cmd = data.cmd;
this.cmdStatus.responses.push(resp);
});
});
setTimeout(this.queryCmdResp, 100);
}
});
}
@@ -381,8 +348,6 @@
ignoreCmdResp() {
this.cmdStatus.execing = 0;
this.cmdStatus.running = {};
this.cmdStatus.respModal = true;
this.cmdStatus.modal = false;
}
+1
View File
@@ -3,6 +3,7 @@
"target": "esnext",
"module": "esnext",
"strict": true,
"noImplicitAny": false,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
+1
View File
@@ -12,5 +12,6 @@ require (
github.com/mattn/go-colorable v0.1.4
github.com/rakyll/statik v0.1.7
github.com/rs/zerolog v1.18.0
github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
)
+13
View File
@@ -1,4 +1,7 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dwdcth/consoleEx v0.0.0-20180521133551-f56f6eb78b76 h1:eObfFy0e/9OQCd5tHy+855jrW7zTihdgIPD7hf2SOQ0=
@@ -41,6 +44,10 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -48,6 +55,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
@@ -69,3 +80,5 @@ gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvR
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+26 -60
View File
@@ -1,7 +1,6 @@
package main
import (
"io/ioutil"
"net"
"net/http"
"path"
@@ -12,7 +11,9 @@ import (
"github.com/rakyll/statik/fs"
"github.com/rs/zerolog/log"
"github.com/zhaojh329/rttys/cache"
"github.com/zhaojh329/rttys/config"
_ "github.com/zhaojh329/rttys/statik"
"github.com/zhaojh329/rttys/utils"
)
type credentials struct {
@@ -28,24 +29,24 @@ func allowOrigin(w http.ResponseWriter) {
w.Header().Set("content-type", "application/json")
}
func httpLogin(cfg *rttysConfig, creds *credentials) bool {
if cfg.httpUsername != creds.Username {
func httpLogin(cfg *config.Config, creds *credentials) bool {
if cfg.HTTPUsername != creds.Username {
return false
}
if cfg.httpPassword != "" {
return cfg.httpPassword == creds.Password
if cfg.HTTPPassword != "" {
return cfg.HTTPPassword == creds.Password
}
return true
}
func authorizedDev(devid string, cfg *rttysConfig) bool {
if cfg.whiteList == nil {
func authorizedDev(devid string, cfg *config.Config) bool {
if cfg.WhiteList == nil {
return true
}
_, ok := cfg.whiteList[devid]
_, ok := cfg.WhiteList[devid]
return ok
}
@@ -86,7 +87,7 @@ func httpStart(br *broker) {
})
authorized.GET("/fontsize/:devid", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"size": cfg.fontSize})
c.JSON(http.StatusOK, gin.H{"size": cfg.FontSize})
})
authorized.POST("/fontsize/:devid", func(c *gin.Context) {
@@ -100,7 +101,7 @@ func httpStart(br *broker) {
return
}
cfg.fontSize = r.Size
cfg.FontSize = r.Size
c.String(http.StatusOK, "OK")
})
@@ -123,8 +124,8 @@ func httpStart(br *broker) {
devs := make([]DeviceInfo, 0)
for id, dev := range br.devices {
dev := DeviceInfo{id, uint32(time.Now().Unix() - dev.timestamp), dev.uptime, dev.desc}
devs = append(devs, dev)
dev := dev.(*device)
devs = append(devs, DeviceInfo{id, uint32(time.Now().Unix() - dev.timestamp), dev.uptime, dev.desc})
}
allowOrigin(c.Writer)
@@ -132,51 +133,14 @@ func httpStart(br *broker) {
c.JSON(http.StatusOK, devs)
})
authorized.GET("/cmd/:devid/:token", func(c *gin.Context) {
allowOrigin(c.Writer)
done := make(chan struct{})
req := &commandReq{
done: done,
w: c.Writer,
}
req.token = c.Param("token")
br.cmdReq <- req
<-done
})
authorized.POST("/cmd/:devid", func(c *gin.Context) {
allowOrigin(c.Writer)
done := make(chan struct{})
req := &commandReq{
done: done,
w: c.Writer,
devid: c.Param("devid"),
}
content, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
sid := jsoniter.Get(content, "sid").ToString()
if _, ok := httpSessions.Get(sid); !ok {
c.Status(http.StatusForbidden)
return
}
req.content = content
br.cmdReq <- req
<-done
handleCmdReq(br, c)
})
r.Any("/web/:devid/:addr/*path", func(c *gin.Context) {
webReqRedirect(br, cfg, c)
webReqRedirect(br, c)
})
r.GET("/authorized/:devid", func(c *gin.Context) {
@@ -196,7 +160,7 @@ func httpStart(br *broker) {
}
if httpLogin(cfg, &creds) {
sid := genUniqueID("http")
sid := utils.GenUniqueID("http")
httpSessions.Set(sid, true, 0)
c.SetCookie("sid", sid, 0, "", "", false, true)
@@ -223,13 +187,15 @@ func httpStart(br *broker) {
http.FileServer(statikFS).ServeHTTP(c.Writer, c.Request)
})
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)
}
go func() {
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)
log.Fatal().Err(err)
}()
}
+67
View File
@@ -0,0 +1,67 @@
package log
import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/dwdcth/consoleEx"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/ssh/terminal"
)
type logFileHook struct {
err error
path string
}
var logFile = &logFileHook{}
func (h *logFileHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
if h.err != nil {
return
}
f, err := os.OpenFile(h.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
h.err = err
log.Fatal().Msg(err.Error())
return
}
defer f.Close()
f.WriteString(zerolog.TimestampFunc().Format(zerolog.TimeFieldFormat) + " |")
f.WriteString(strings.ToUpper(level.String()) + "| ")
_, file, line, ok := runtime.Caller(3)
if ok {
f.WriteString(zerolog.CallerMarshalFunc(file, line) + " |")
}
f.WriteString(msg)
f.WriteString("\n")
}
func init() {
zerolog.CallerMarshalFunc = func(file string, line int) string {
return filepath.Base(file) + ":" + strconv.Itoa(line)
}
out := consoleEx.ConsoleWriterEx{Out: colorable.NewColorableStdout()}
logger := zerolog.New(out).With().Caller().Timestamp().Logger()
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
logger = logger.Hook(logFile)
}
log.Logger = logger
}
// SetPath set the log file path
func SetPath(path string) {
logFile.path = path
}
+119 -70
View File
@@ -1,80 +1,26 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/dwdcth/consoleEx"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"github.com/zhaojh329/rttys/config"
rlog "github.com/zhaojh329/rttys/log"
"github.com/zhaojh329/rttys/utils"
"github.com/zhaojh329/rttys/version"
"golang.org/x/crypto/ssh/terminal"
)
type logFileHook struct {
err error
path string
}
func runRttys(c *cli.Context) {
rlog.SetPath(c.String("log"))
var logFile = &logFileHook{}
cfg := config.Parse(c)
func (h *logFileHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
if h.err != nil {
return
}
f, err := os.OpenFile(h.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
h.err = err
log.Fatal().Msg(err.Error())
return
}
defer f.Close()
f.WriteString(zerolog.TimestampFunc().Format(zerolog.TimeFieldFormat) + " |")
f.WriteString(strings.ToUpper(level.String()) + "| ")
_, file, line, ok := runtime.Caller(3)
if ok {
f.WriteString(zerolog.CallerMarshalFunc(file, line) + " |")
}
f.WriteString(msg)
f.WriteString("\n")
}
func init() {
zerolog.CallerMarshalFunc = func(file string, line int) string {
return filepath.Base(file) + ":" + strconv.Itoa(line)
}
out := consoleEx.ConsoleWriterEx{Out: colorable.NewColorableStdout()}
logger := zerolog.New(out).With().Caller().Timestamp().Logger()
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
logger = logger.Hook(logFile)
}
log.Logger = logger
}
func main() {
if runtime.GOOS == "windows" {
flag.StringVar(&logFile.path, "log", "rttys.log", "log file path")
} else {
flag.StringVar(&logFile.path, "log", "/var/log/rttys.log", "log file path")
}
cfg := parseConfig()
if cfg.httpUsername == "" {
log.Fatal().Msg("You must configure the http username by commandline or config file")
if cfg.HTTPUsername == "" {
fmt.Println("You must configure the http username by commandline or config file")
os.Exit(1)
}
log.Info().Msg("Go Version: " + runtime.Version())
@@ -96,11 +42,114 @@ func main() {
br := newBroker(cfg)
go br.run()
go listenDevice(br)
go listenDeviceWeb(br)
go httpStart(br)
listenDevice(br)
listenDeviceWeb(br)
httpStart(br)
for {
time.Sleep(time.Second)
select {}
}
func main() {
defaultLogPath := "/var/log/rttys.log"
if runtime.GOOS == "windows" {
defaultLogPath = "rttys.log"
}
app := &cli.App{
Name: "rttys",
Usage: "The server side for rtty",
Version: version.Version(),
Commands: []*cli.Command{
{
Name: "run",
Usage: "Run rttys",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "log",
Value: defaultLogPath,
Usage: "log file path",
},
&cli.StringFlag{
Name: "conf",
Aliases: []string{"c"},
Value: "./rttys.conf",
Usage: "config file to load",
},
&cli.StringFlag{
Name: "addr-dev",
Value: ":5912",
Usage: "address to listen device",
},
&cli.StringFlag{
Name: "addr-user",
Value: ":5913",
Usage: "address to listen user",
},
&cli.StringFlag{
Name: "addr-web",
Value: ":5914",
Usage: "address to listen for access device's web",
},
&cli.StringFlag{
Name: "web-redir-url",
Value: "",
Usage: "url to redirect for access device's web",
},
&cli.StringFlag{
Name: "ssl-cert",
Value: "",
Usage: "ssl cert file Path",
},
&cli.StringFlag{
Name: "ssl-key",
Value: "",
Usage: "ssl key file Path",
},
&cli.StringFlag{
Name: "http-username",
Value: "",
Usage: "username for http auth",
},
&cli.StringFlag{
Name: "http-password",
Value: "",
Usage: "password for http auth",
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Value: "",
Usage: "token to use",
},
&cli.StringFlag{
Name: "white-list",
Value: "",
Usage: "white list(device IDs separated by spaces or *)",
},
},
Action: func(c *cli.Context) error {
runRttys(c)
return nil
},
},
{
Name: "token",
Usage: "Generate a token",
Action: func(c *cli.Context) error {
utils.GenToken()
return nil
},
},
},
Action: func(c *cli.Context) error {
c.App.Command("run").Run(c)
return nil
},
}
err := app.Run(os.Args)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -13
View File
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"github.com/zhaojh329/rttys/client"
"net/http"
)
@@ -27,9 +28,9 @@ type user struct {
}
type usrMessage struct {
sid string
msgType int
data []byte
sid string
typ int
data []byte
}
var upgrader = websocket.Upgrader{
@@ -38,11 +39,19 @@ var upgrader = websocket.Upgrader{
},
}
func (u *user) writeMessage(messageType int, data []byte) {
u.conn.WriteMessage(messageType, data)
func (u *user) IsDevice() bool {
return false
}
func (u *user) close() {
func (u *user) DeviceID() string {
return u.devid
}
func (u *user) WriteMsg(typ int, data []byte) {
u.conn.WriteMessage(typ, data)
}
func (u *user) Close() {
defer u.closeMutex.Unlock()
u.closeMutex.Lock()
@@ -50,20 +59,20 @@ func (u *user) close() {
if !u.closed {
u.closed = true
u.conn.Close()
u.br.logout <- u.sid
u.br.unregister <- u
}
}
func (u *user) loginAck(code int) {
func userLoginAck(code int, c client.Client) {
msg := fmt.Sprintf(`{"type":"login","err":%d}`, code)
u.writeMessage(websocket.TextMessage, []byte(msg))
c.WriteMsg(websocket.TextMessage, []byte(msg))
}
func (u *user) readLoop() {
defer u.close()
defer u.Close()
for {
msgType, data, err := u.conn.ReadMessage()
typ, data, err := u.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Error().Msg(err.Error())
@@ -71,7 +80,7 @@ func (u *user) readLoop() {
break
}
u.br.userMessage <- &usrMessage{u.sid, msgType, data}
u.br.userMessage <- &usrMessage{u.sid, typ, data}
}
}
@@ -97,5 +106,5 @@ func serveUser(br *broker, c *gin.Context) {
go u.readLoop()
br.login <- u
br.register <- u
}
+6 -6
View File
@@ -1,4 +1,4 @@
package main
package utils
import (
"crypto/md5"
@@ -14,7 +14,8 @@ import (
"github.com/rs/zerolog/log"
)
func genUniqueID(extra string) string {
// GenUniqueID generate a unique ID
func GenUniqueID(extra string) string {
buf := make([]byte, 20)
binary.BigEndian.PutUint32(buf, uint32(time.Now().Unix()))
@@ -27,15 +28,14 @@ func genUniqueID(extra string) string {
return hex.EncodeToString(h.Sum(nil))
}
func genTokenAndExit() {
// GenToken generate a token
func GenToken() {
password, err := gopass.GetPasswdPrompt("Please set a password:", true, os.Stdin, os.Stdout)
if err != nil {
log.Fatal().Msg(err.Error())
}
token := genUniqueID(string(password))
token := GenUniqueID(string(password))
fmt.Println("Your token is:", token)
os.Exit(0)
}
+71 -61
View File
@@ -2,6 +2,7 @@ package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
@@ -15,8 +16,15 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/zhaojh329/rttys/cache"
"github.com/zhaojh329/rttys/client"
"github.com/zhaojh329/rttys/utils"
)
type webSession struct {
ctx context.Context
cancel context.CancelFunc
}
type webNewCon struct {
r *http.Request // First request
b *bufio.Reader
@@ -24,19 +32,19 @@ type webNewCon struct {
}
type webCon struct {
dev *device
dev client.Client
c net.Conn
}
type webReq struct {
addr []byte
data []byte
dev *device
dev client.Client
}
type webResp struct {
data []byte
dev *device
dev client.Client
}
var webCons = make(map[string]map[string]*webCon)
@@ -46,11 +54,11 @@ func handleWebReq(req *webReq) {
dev := req.dev
if req.data == nil {
delete(webCons[dev.id], string(req.addr))
delete(webCons[dev.DeviceID()], string(req.addr))
return
}
dev.writeMsg(msgTypeWeb, req.data)
dev.WriteMsg(msgTypeWeb, req.data)
}
func handleWebResp(resp *webResp) {
@@ -62,7 +70,7 @@ func handleWebResp(resp *webResp) {
return
}
devcons, ok := webCons[resp.dev.id]
devcons, ok := webCons[resp.dev.DeviceID()]
if !ok {
return
}
@@ -77,7 +85,7 @@ func handleWebResp(resp *webResp) {
c.Write(data)
}
func makeWebReqMsg(br *broker, dev *device, srcAddr []byte, r *http.Request, hostHeaderRewrite string, destAddr []byte) {
func makeWebReqMsg(br *broker, dev client.Client, srcAddr []byte, r *http.Request, hostHeaderRewrite string, destAddr []byte) {
req := append([]byte{}, srcAddr...)
req = append(req, destAddr...)
req = append(req, r.Method...)
@@ -150,23 +158,7 @@ func handleWebCon(br *broker, wc *webNewCon) {
c := wc.c
r := wc.r
cookie, err := r.Cookie("rtty-web-sid")
if err != nil {
c.Close()
return
}
sid := cookie.Value
var done chan struct{}
if v, ok := webSessions.Get(sid); ok {
webSessions.Active(sid, 0)
done = v.(chan struct{})
} else {
c.Close()
return
}
cookie, err = r.Cookie("rtty-web-devid")
cookie, err := r.Cookie("rtty-web-devid")
if err != nil {
c.Close()
return
@@ -179,6 +171,24 @@ func handleWebCon(br *broker, wc *webNewCon) {
return
}
cookie, err = r.Cookie("rtty-web-sid")
if err != nil {
c.Close()
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 {
c.Close()
return
}
hostHeaderRewrite := "localhost"
cookie, err = r.Cookie("rtty-web-destaddr")
if err == nil {
@@ -195,13 +205,10 @@ func handleWebCon(br *broker, wc *webNewCon) {
webCons[devid][string(srcAddr)] = &webCon{dev, c}
readEnd := make(chan struct{})
go func() {
defer func() {
br.webReq <- &webReq{srcAddr, nil, dev}
c.Close()
close(readEnd)
cancel()
}()
makeWebReqMsg(br, dev, srcAddr, r, hostHeaderRewrite, destAddr)
@@ -217,51 +224,53 @@ func handleWebCon(br *broker, wc *webNewCon) {
go func() {
select {
case <-done:
case <-ctx.Done():
c.Close()
case <-readEnd:
}
}()
}
func listenDeviceWeb(br *broker) error {
func listenDeviceWeb(br *broker) {
cfg := br.cfg
addr, err := net.ResolveTCPAddr("tcp", cfg.addrWeb)
addr, err := net.ResolveTCPAddr("tcp", cfg.AddrWeb)
if err != nil {
return err
log.Fatal().Msg(err.Error())
}
cfg.webPort = addr.Port
cfg.WebPort = addr.Port
webSessions = cache.New(30*time.Minute, 5*time.Second)
log.Info().Msgf("Listen dev web on: %s", cfg.addrWeb)
log.Info().Msgf("Listen dev web on: %s", cfg.AddrWeb)
ln, err := net.Listen("tcp", cfg.addrWeb)
ln, err := net.Listen("tcp", cfg.AddrWeb)
if err != nil {
return err
log.Fatal().Msg(err.Error())
}
defer ln.Close()
for {
c, err := ln.Accept()
if err != nil {
log.Error().Msg(err.Error())
continue
}
go func() {
defer ln.Close()
go func() {
b := bufio.NewReader(c)
r, err := http.ReadRequest(b)
for {
c, err := ln.Accept()
if err != nil {
c.Close()
return
log.Error().Msg(err.Error())
continue
}
br.webCon <- &webNewCon{r, b, c}
}()
}
go func() {
b := bufio.NewReader(c)
r, err := http.ReadRequest(b)
if err != nil {
c.Close()
return
}
br.webCon <- &webNewCon{r, b, c}
}()
}
}()
}
func webReqVaildAddr(addr string) (net.IP, uint16, error) {
@@ -286,7 +295,8 @@ func webReqVaildAddr(addr string) (net.IP, uint16, error) {
return ip, uint16(port), nil
}
func webReqRedirect(br *broker, cfg *rttysConfig, c *gin.Context) {
func webReqRedirect(br *broker, c *gin.Context) {
cfg := br.cfg
devid := c.Param("devid")
addr := c.Param("addr")
path := c.Param("path")
@@ -303,7 +313,7 @@ func webReqRedirect(br *broker, cfg *rttysConfig, c *gin.Context) {
return
}
location := cfg.webRedirUrl
location := cfg.WebRedirURL
if location == "" {
host, _, err := net.SplitHostPort(c.Request.Host)
@@ -311,8 +321,8 @@ func webReqRedirect(br *broker, cfg *rttysConfig, c *gin.Context) {
host = c.Request.Host
}
location = "http://" + host
if cfg.webPort != 80 {
location += fmt.Sprintf(":%d", cfg.webPort)
if cfg.WebPort != 80 {
location += fmt.Sprintf(":%d", cfg.WebPort)
}
}
@@ -323,15 +333,15 @@ func webReqRedirect(br *broker, cfg *rttysConfig, c *gin.Context) {
sid, err := c.Cookie("rtty-web-sid")
if err == nil {
if v, ok := webSessions.Get(sid); ok {
ch := v.(chan struct{})
v.(*webSession).cancel()
webSessions.Del(sid)
close(ch)
}
}
sid = genUniqueID("web")
sid = utils.GenUniqueID("web")
webSessions.Set(sid, make(chan struct{}), 0)
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)