mirror of
https://github.com/zhaojh329/rttys.git
synced 2026-02-27 09:53:21 +08:00
@@ -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
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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():
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user