Optimize code

Transfer tty data in Binary;
Transfer control data in Json Text;

Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
This commit is contained in:
Jianhui Zhao
2018-10-02 15:30:07 +08:00
parent 3a722cf276
commit ebbd062c8b
10 changed files with 582 additions and 973 deletions
+138 -162
View File
@@ -20,192 +20,168 @@
package main
import (
"time"
"strconv"
"math/rand"
"crypto/md5"
"encoding/hex"
"github.com/gorilla/websocket"
"github.com/golang/protobuf/proto"
"github.com/zhaojh329/rttys/rtty"
"fmt"
"time"
"github.com/buger/jsonparser"
"github.com/gorilla/websocket"
)
const RTTY_MESSAGE_VERSION = 2
const RTTY_MAX_SESSION_ID = 1000000
type Broker struct {
devices map[string]*Client
sessions map[string]*Session
// Join requests from the clients.
join chan *Client
// Leave requests from clients.
leave chan *Client
// Buffered channel of inbound messages from device.
inDevMessage chan *wsMessage
// Buffered channel of inbound messages from user.
inUsrMessage chan *wsMessage
devices map[string]*Client
sessions map[uint32]*Session
register chan *Client /* Register requests from the clients. */
unregister chan *Client /* Unregister requests from clients. */
inMessage chan *wsInMessage /* Buffered channel of inbound messages. */
}
type Session struct {
dev *Client
user *Client
dev *Client
web *Client
devsid uint8
}
func newBroker() *Broker {
return &Broker{
join: make(chan *Client, 100),
leave: make(chan *Client, 100),
devices: make(map[string]*Client),
sessions: make(map[string]*Session),
inDevMessage: make(chan *wsMessage, 100),
inUsrMessage: make(chan *wsMessage, 100),
}
return &Broker{
register: make(chan *Client, 100),
unregister: make(chan *Client, 100),
devices: make(map[string]*Client),
sessions: make(map[uint32]*Session),
inMessage: make(chan *wsInMessage, 1000),
}
}
func RttyMessageInit(msg *rtty.RttyMessage) []byte {
data, _ := proto.Marshal(msg)
return data
// return 0 for failed
func getFreeSid(br *Broker) uint32 {
for sid := uint32(1); sid <= RTTY_MAX_SESSION_ID; sid++ {
if _, ok := br.sessions[sid]; !ok {
return sid
}
}
return 0
}
func generateSID(devid string) string {
md5Ctx := md5.New()
md5Ctx.Write([]byte(devid + strconv.FormatFloat(rand.Float64(), 'e', 6, 32)))
cipherStr := md5Ctx.Sum(nil)
return hex.EncodeToString(cipherStr)
}
func (br *Broker) newSession(web *Client) bool {
devid := web.devid
sid := getFreeSid(br)
func (br *Broker) newSession(user *Client) bool {
devid := user.devid
sid := generateSID(devid)
if sid < 1 {
rlog.Println("Not found available sid")
return false
}
if dev, ok := br.devices[devid]; ok {
br.sessions[sid] = &Session{dev, user}
user.sid = sid
if dev, ok := br.devices[devid]; ok {
devsid := dev.getFreeSid()
if devsid < 1 {
rlog.Println("Not found available devsid")
return false
}
// Write to user
msg := RttyMessageInit(&rtty.RttyMessage{
Version: RTTY_MESSAGE_VERSION,
Type: rtty.RttyMessage_LOGINACK,
Sid: sid,
Code: rtty.RttyMessage_LoginCode_value["OK"],
})
user.wsWrite(websocket.BinaryMessage, msg)
br.sessions[sid] = &Session{dev, web, devsid}
dev.sessions[devsid] = sid
web.sid = sid
// Write to device
msg = RttyMessageInit(&rtty.RttyMessage{
Version: RTTY_MESSAGE_VERSION,
Type: rtty.RttyMessage_LOGIN,
Sid: sid,
})
dev.wsWrite(websocket.BinaryMessage, msg)
rlog.Println("New session:", sid)
return true
} else {
// Write to user
msg := RttyMessageInit(&rtty.RttyMessage{
Version: RTTY_MESSAGE_VERSION,
Type: rtty.RttyMessage_LOGINACK,
Sid: sid,
Code: rtty.RttyMessage_LoginCode_value["OFFLINE"],
})
user.wsWrite(websocket.BinaryMessage, msg)
msg := fmt.Sprintf(`{"type":"login","sid":%d}`, devsid)
rlog.Println("Device", devid, "offline")
return false
}
}
// Notify the device to create a pty and associate it with a session id
dev.wsWrite(websocket.TextMessage, []byte(msg))
func delSession(sessions map[string]*Session, sid string) {
if session, ok := sessions[sid]; ok {
delete(sessions, sid)
session.user.wsClose()
rlog.Println("Delete session: ", sid)
if session.dev != nil {
msg := RttyMessageInit(&rtty.RttyMessage{
Version: RTTY_MESSAGE_VERSION,
Type: rtty.RttyMessage_LOGOUT,
Sid: sid,
})
session.dev.wsWrite(websocket.BinaryMessage, msg)
}
}
}
func dispatchMsg(data []byte, isDev bool, br *Broker) {
msg := &rtty.RttyMessage{};
proto.Unmarshal(data, msg);
if msg.Type == rtty.RttyMessage_COMMAND {
cmdMutex.Lock()
if cmd, ok := command[msg.Id]; ok {
cmd <- msg
}
cmdMutex.Unlock()
return;
}
if session, ok := br.sessions[msg.Sid]; ok {
if msg.Type == rtty.RttyMessage_LOGOUT {
session.dev = nil
delSession(br.sessions, msg.Sid)
return
}
if isDev {
session.user.wsWrite(websocket.BinaryMessage, data)
} else {
session.dev.wsWrite(websocket.BinaryMessage, data)
}
}
rlog.Println("New session:", sid)
return true
} else {
// Notify the user that the device is offline
msg := `{"type":"login","err":1,"msg":"offline"}`
web.wsWrite(websocket.TextMessage, []byte(msg))
rlog.Println("Device", devid, "offline")
return false
}
}
func (br *Broker) run() {
for {
select {
case client := <- br.join:
if client.isDev {
if _, ok := br.devices[client.devid]; ok {
rlog.Println("ID conflicting:", client.devid)
client.wsClose();
} else {
client.isJoined = true
br.devices[client.devid] = client
rlog.Printf("New device:id('%s'), description('%s')", client.devid, client.description)
}
} else {
// From user browse
if !br.newSession(client) {
time.AfterFunc(500 * time.Millisecond, client.wsClose)
}
}
case client := <- br.leave:
if client.isDev {
client.wsClose()
for {
select {
case c := <-br.register:
if c.isDev {
if _, ok := br.devices[c.devid]; ok {
rlog.Println("ID conflicting:", c.devid)
c.wsClose()
} else {
br.devices[c.devid] = c
rlog.Printf("New device:id('%s'), description('%s')", c.devid, c.desc)
}
} else {
// From user
if !br.newSession(c) {
time.AfterFunc(500*time.Millisecond, c.wsClose)
}
}
case c := <-br.unregister:
if c.isDev {
c.wsClose()
if dev, ok := br.devices[client.devid]; ok {
rlog.Printf("Dead device:id('%s'), description('%s')", dev.devid, dev.description)
delete(br.devices, dev.devid)
}
if dev, ok := br.devices[c.devid]; ok {
rlog.Printf("Dead device:id('%s'), description('%s')", dev.devid, dev.desc)
delete(br.devices, dev.devid)
}
for sid, session := range br.sessions {
if session.dev.devid == client.devid {
session.dev = nil
delSession(br.sessions, sid)
}
}
} else {
delSession(br.sessions, client.sid)
}
case msg := <- br.inDevMessage:
dispatchMsg(msg.data, true, br)
case msg := <- br.inUsrMessage:
dispatchMsg(msg.data, false, br)
}
}
for sid, session := range br.sessions {
if session.dev.devid == c.devid {
session.web.wsClose()
delete(br.sessions, sid)
rlog.Println("Delete session: ", sid)
}
}
} else {
if session, ok := br.sessions[c.sid]; ok {
msg := fmt.Sprintf(`{"type":"logout","sid":%d}`, session.devsid)
session.dev.wsWrite(websocket.TextMessage, []byte(msg))
delete(br.sessions, c.sid)
delete(session.dev.sessions, session.devsid)
rlog.Println("Delete session: ", c.sid)
}
}
case msg := <-br.inMessage:
msgType := msg.msgType
data := msg.data
var sid uint32
c := msg.c
if c.isDev {
var devsid uint8
if msgType == websocket.BinaryMessage {
devsid = data[0]
data = data[1:]
} else {
tp, _ := jsonparser.GetString(data, "type")
if tp == "cmd" {
handleCmdResp(data)
continue
}
val, _ := jsonparser.GetInt(data, "sid")
devsid = uint8(val)
}
sid = c.sessions[devsid]
} else {
sid = c.sid
}
if session, ok := br.sessions[sid]; ok {
if c.isDev {
c = session.web
} else {
if msgType == websocket.BinaryMessage {
sb := make([]byte, 1)
sb[0] = session.devsid
data = append(sb, data...)
}
c = session.dev
}
c.wsWrite(msgType, data)
}
}
}
}
+171 -162
View File
@@ -20,214 +20,223 @@
package main
import (
"time"
"sync"
"errors"
"strconv"
"net/http"
"github.com/gorilla/websocket"
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/gorilla/websocket"
)
const (
RTTY_PROTO_VERSION = 1
/* Match the version with the device */
RTTY_PROTO_VERSION = 1
// Max lose ping times
aliveTimes = 3
/* Max lose ping times */
RTTY_MAX_LOSE_PING = 3
/* Max session id for each device */
RTTY_MAX_SESSION_ID_DEV = 5
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type wsMessage struct {
msgType int
data []byte
}
// Representing a device or user browser
type Client struct {
br *Broker
isDev bool
// device description
description string
devid string
// Registration time
timestamp int64
sid string
conn *websocket.Conn
// Buffered channel of outbound messages.
outMessage chan *wsMessage
conn *websocket.Conn
br *Broker
devid string
desc string /* description for device */
isDev bool
timestamp int64 /* Registration time */
mutex sync.Mutex /* Avoid repeated closes and concurrent map writes */
closed bool
closeChan chan byte
alive uint32
sessions map[uint8]uint32
sid uint32
outMessage chan *wsOutMessage /* Buffered channel of outbound messages */
}
cmdid uint32
cmd map[uint32]chan *wsMessage
type wsInMessage struct {
msgType int
data []byte
c *Client
}
isJoined bool
type wsOutMessage struct {
msgType int
data []byte
}
// Avoid repeated closes and concurrent map writes
mutex sync.Mutex
isClosed bool
closeChan chan byte
alive uint32
func (c *Client) getFreeSid() uint8 {
for sid := uint8(1); sid <= RTTY_MAX_SESSION_ID_DEV; sid++ {
if _, ok := c.sessions[sid]; !ok {
return sid
}
}
return uint8(0)
}
func (c *Client) wsClose() {
defer c.mutex.Unlock()
c.mutex.Lock()
defer c.mutex.Unlock()
c.mutex.Lock()
if !c.isClosed {
c.conn.Close()
c.isClosed = true
close(c.closeChan)
}
}
func (c *Client) leave() {
if c.isJoined {
c.br.leave <- c
}
if !c.closed {
c.conn.Close()
c.closed = true
close(c.closeChan)
}
}
func (c *Client) wsWrite(messageType int, data []byte) error {
select {
case c.outMessage <- &wsMessage{messageType, data}:
case <- c.closeChan:
return errors.New("websocket closed")
}
return nil
func (c *Client) unregister() {
c.br.unregister <- c
}
func (c *Client) wsWrite(msgType int, data []byte) error {
select {
case c.outMessage <- &wsOutMessage{msgType, data}:
case <-c.closeChan:
return errors.New("websocket closed")
}
return nil
}
func (c *Client) readPump() {
defer func() {
c.leave()
}()
defer func() {
c.unregister()
}()
for {
msgType, data, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
rlog.Printf("error: %v", err)
}
break
}
for {
msgType, data, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
rlog.Printf("error: %v", err)
}
break
}
msg := &wsMessage{msgType, data}
msg := &wsInMessage{msgType, data, c}
inMessage := c.br.inUsrMessage
if c.isDev {
inMessage = c.br.inDevMessage
}
select {
case inMessage <- msg:
case <- c.closeChan:
return
}
}
select {
case c.br.inMessage <- msg:
case <-c.closeChan:
return
}
}
}
func (c *Client) writePump() {
defer func() {
c.leave()
}()
defer func() {
c.unregister()
}()
for {
select {
case msg := <- c.outMessage:
if err := c.conn.WriteMessage(msg.msgType, msg.data); err != nil {
return
}
case <- c.closeChan:
return
}
}
for {
select {
case msg := <-c.outMessage:
if err := c.conn.WriteMessage(msg.msgType, msg.data); err != nil {
return
}
case <-c.closeChan:
return
}
}
}
func (c *Client) keepAlive(keepalive int64) {
ticker := time.NewTicker(time.Second)
last := time.Now().Unix()
keepalive = keepalive + 3
alive := aliveTimes
ticker := time.NewTicker(time.Second)
last := time.Now().Unix()
keepalive = keepalive + 3
alive := RTTY_MAX_LOSE_PING
defer func() {
c.leave()
}()
defer func() {
c.unregister()
}()
// Get the current ping handler
pingHandler := c.conn.PingHandler()
// Get the current ping handler
pingHandler := c.conn.PingHandler()
c.conn.SetPingHandler(func(appData string) error {
alive = aliveTimes
last = time.Now().Unix()
return pingHandler(appData)
})
c.conn.SetPingHandler(func(appData string) error {
alive = RTTY_MAX_LOSE_PING
last = time.Now().Unix()
return pingHandler(appData)
})
for {
select {
case <- c.closeChan:
return
case <- ticker.C:
now := time.Now().Unix()
if now - last > keepalive {
alive--
last = now
if alive == 0 {
rlog.Printf("Inactive device in long time, now kill it(%s)\n", c.devid)
return
}
}
}
}
for {
select {
case <-c.closeChan:
return
case <-ticker.C:
now := time.Now().Unix()
if now-last > keepalive {
alive--
last = now
if alive == 0 {
rlog.Printf("Inactive device in long time, now kill it(%s)\n", c.devid)
return
}
}
}
}
}
/* serveWs handles websocket requests from the peer. */
func serveWs(br *Broker, w http.ResponseWriter, r *http.Request) {
keepalive, _ := strconv.ParseInt(r.URL.Query().Get("keepalive"), 10, 64)
proto,_ := strconv.Atoi(r.URL.Query().Get("proto"))
isDev := r.URL.Query().Get("device") != ""
devid := r.URL.Query().Get("devid")
keepalive, _ := strconv.ParseInt(r.URL.Query().Get("keepalive"), 10, 64)
proto, _ := strconv.Atoi(r.URL.Query().Get("proto"))
isDev := r.URL.Query().Get("device") != ""
devid := r.URL.Query().Get("devid")
if devid == "" {
rlog.Println("devid required")
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
rlog.Println(err)
return
}
if isDev {
if proto != RTTY_PROTO_VERSION {
rlog.Printf("proto number is not matched for device '%s', you need to update your server(rttys) or client(rtty) or both them", devid)
return
}
}
if devid == "" {
msg := fmt.Sprintf(`{"type":"register","err":1,"msg":"devid required"}`)
conn.WriteMessage(websocket.TextMessage, []byte(msg))
rlog.Println("devid required")
conn.Close()
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
rlog.Println(err)
return
}
if isDev {
if proto != RTTY_PROTO_VERSION {
msg := fmt.Sprintf(`{"type":"register","err":1,"msg":"proto number is not matched"}`)
conn.WriteMessage(websocket.TextMessage, []byte(msg))
rlog.Printf("proto number is not matched for device '%s', you need to update your server(rttys) or client(rtty) or both them", devid)
conn.Close()
return
}
}
client := &Client{
br: br,
devid: devid,
conn: conn,
timestamp: time.Now().Unix(),
outMessage: make(chan *wsMessage, 100),
closeChan: make(chan byte),
isClosed: false,
}
client := &Client{
br: br,
conn: conn,
devid: devid,
timestamp: time.Now().Unix(),
outMessage: make(chan *wsOutMessage, 1000),
closeChan: make(chan byte),
}
if isDev {
client.isDev = true
client.description = r.URL.Query().Get("description")
client.cmd = make(map[uint32]chan *wsMessage)
}
if isDev {
client.isDev = true
client.sessions = make(map[uint8]uint32)
client.desc = r.URL.Query().Get("description")
client.br.join <- client
if keepalive > 0 {
go client.keepAlive(keepalive)
}
}
go client.readPump()
go client.writePump()
go client.readPump()
go client.writePump()
if client.isDev && keepalive > 0 {
go client.keepAlive(keepalive)
}
client.br.register <- client
}
+92 -92
View File
@@ -20,110 +20,110 @@
package main
import (
"time"
"sync"
"net/http"
"io/ioutil"
"encoding/json"
"github.com/gorilla/websocket"
"github.com/zhaojh329/rttys/rtty"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
"github.com/buger/jsonparser"
"github.com/gorilla/websocket"
)
type CommandReq struct {
Username string `json:"username"`
Password string `json:"password"`
Devid string `json:"devid"`
Cmd string `json:"cmd"`
Params []string `json:"params"`
Env map[string]string `json:"env"`
const RTTY_MAX_CMD_ID = 1000000
const (
RTTY_CMD_ERR_INVALID = 1001
RTTY_CMD_ERR_OFFLINE = 1002
RTTY_CMD_ERR_BUSY = 1003
RTTY_CMD_ERR_TIMEOUT = 1004
)
var cmdErrMsg = map[int]string{
RTTY_CMD_ERR_INVALID: "invalid format",
RTTY_CMD_ERR_OFFLINE: "device offline",
RTTY_CMD_ERR_BUSY: "server is busy",
RTTY_CMD_ERR_TIMEOUT: "timeout",
}
type CommandResult struct {
ID uint32 `json:"id,omitempty"`
Err int32 `json:err`
Msg string `json:"msg"`
Code int32 `json:"code"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
var commands = struct {
sync.Mutex
m map[uint32]chan []byte
}{m: make(map[uint32]chan []byte)}
// return nil for failed
func getFreeCmdChan() (chan []byte, uint32) {
defer func() {
commands.Unlock()
}()
commands.Lock()
for id := uint32(1); id <= RTTY_MAX_CMD_ID; id++ {
_, ok := commands.m[id]
if !ok {
ch := make(chan []byte)
commands.m[id] = ch
return ch, id
}
}
return nil, 0
}
var commandID uint32 = 0
var cmdMutex sync.Mutex
var command = make(map[uint32]chan *rtty.RttyMessage)
func freeCmd(id uint32) {
commands.Lock()
delete(commands.m, id)
commands.Unlock()
}
func handleCmdResp(data []byte) {
id, _ := jsonparser.GetInt(data, "id")
if ch, ok := commands.m[uint32(id)]; ok {
ch <- data
}
}
func cmdErrReply(err int, w http.ResponseWriter) {
msg := fmt.Sprintf(`{"err": %d, "msg":"%s"}`, err, cmdErrMsg[err])
w.Write([]byte(msg))
}
func serveCmd(br *Broker, w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(time.Second * 10)
defer func() {
ticker.Stop()
}()
timer := time.NewTimer(time.Second * 5)
defer func() {
timer.Stop()
}()
err := rtty.RttyMessage_CommandErr_value["NONE"]
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
devid, err := jsonparser.GetString(body, "devid")
if err != nil {
cmdErrReply(RTTY_CMD_ERR_INVALID, w)
return
}
req := CommandReq{}
json.Unmarshal(body, &req)
dev, ok := br.devices[devid]
if !ok {
cmdErrReply(RTTY_CMD_ERR_OFFLINE, w)
return
}
if req.Devid == "" {
err = rtty.RttyMessage_CommandErr_value["DEVID_REQUIRED"]
} else if req.Cmd == "" {
err = rtty.RttyMessage_CommandErr_value["CMD_REQUIRED"]
} else if dev, ok := br.devices[req.Devid]; !ok {
err = rtty.RttyMessage_CommandErr_value["DEV_OFFLINE"]
} else {
cmdMutex.Lock()
id := commandID
command[id] = make(chan *rtty.RttyMessage)
commandID = commandID + 1
if commandID == 1024 {
commandID = 0
}
cmd := command[id]
cmdMutex.Unlock()
cmdChan, id := getFreeCmdChan()
if cmdChan == nil {
cmdErrReply(RTTY_CMD_ERR_BUSY, w)
return
}
msg := RttyMessageInit(&rtty.RttyMessage{
Version: RTTY_MESSAGE_VERSION,
Type: rtty.RttyMessage_COMMAND,
Id: id,
Name: req.Cmd,
Username: req.Username,
Password: req.Password,
Params: req.Params,
Env: req.Env,
})
msg := fmt.Sprintf(`{"type":"cmd","id":%d,"attrs":%s}`, id, string(body))
dev.wsWrite(websocket.TextMessage, []byte(msg))
dev.wsWrite(websocket.BinaryMessage, msg)
select {
case msg := <- cmd:
res := CommandResult{
Err: msg.Err,
Msg: rtty.RttyMessage_CommandErr_name[msg.Err],
Code: msg.Code,
Stdout: msg.StdOut,
Stderr: msg.StdErr,
}
cmdMutex.Lock()
delete(command, msg.Id)
cmdMutex.Unlock()
js, _ := json.Marshal(res)
w.Write(js)
return
case <- ticker.C:
cmdMutex.Lock()
delete(command, id)
cmdMutex.Unlock()
err = rtty.RttyMessage_CommandErr_value["TIMEOUT"]
goto Err
}
}
Err:
res := CommandResult{Err: err, Msg: rtty.RttyMessage_CommandErr_name[err]}
js, _ := json.Marshal(res)
w.Write(js)
select {
case data := <-cmdChan:
attrs, _, _, _ := jsonparser.Get(data, "attrs")
w.Write(attrs)
case <-timer.C:
freeCmd(id)
cmdErrReply(RTTY_CMD_ERR_TIMEOUT, w)
}
}
-2
View File
@@ -12,8 +12,6 @@
"dependencies": {
"axios": "^0.18.0",
"iview": "^2.14.1",
"pbf": "^3.1.0",
"simple-websocket": "^7.0.2",
"string-format-easy": "^1.0.1",
"vue": "^2.5.13",
"vue-axios": "^2.1.1",
+51 -62
View File
@@ -17,30 +17,15 @@
<script>
import * as Socket from 'simple-websocket';
import { Terminal } from 'xterm'
import 'xterm/lib/xterm.css'
import * as fit from 'xterm/lib/addons/fit/fit';
import * as overlay from '@/overlay';
import Utf8ArrayToStr from '@/utf8array_str'
import 'zmodem.js/dist/zmodem.devel'
Terminal.applyAddon(fit);
Terminal.applyAddon(overlay);
const Pbf = require('pbf');
const rttyMsg = require('@/rtty.proto').rtty_message;
function rttyMsgInit(type, msg) {
let pbf = new Pbf();
msg.version = 2;
msg.type = rttyMsg.Type[type].value;
rttyMsg.write(msg, pbf);
return pbf.finish();
}
export default {
name: 'Rtty',
data() {
@@ -59,7 +44,7 @@ export default {
methods: {
logout() {
if (this.ws) {
this.ws.destroy();
this.ws.close();
delete this.ws;
}
@@ -179,8 +164,7 @@ export default {
zsession.on("session_end", () => {
this.term.write('\n');
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from('\n')});
this.ws.send(msg);
this.ws.send(Buffer.from('\n'));
});
zsession.start();
@@ -253,10 +237,12 @@ export default {
this.username = this.$route.query.username;
this.password = this.$route.query.password;
let ws = new Socket(protocol + location.host + '/ws?devid=' + devid);
this.ws = ws;
let ws = new WebSocket(protocol + location.host + '/ws?devid=' + devid);
ws.onopen = () => {
ws.binaryType = 'arraybuffer';
this.ws = ws;
ws.on('connect', () => {
let term = new Terminal({
cursorBlink: true,
fontSize: 16
@@ -276,8 +262,8 @@ export default {
term.on('resize', (size) => {
setTimeout(() => {
let msg = rttyMsgInit('WINSIZE', {sid: this.sid, cols: size.cols, rows: size.rows});
ws.send(msg);
let msg = {type: "winsize", sid: this.sid, cols: size.cols, rows: size.rows};
ws.send(JSON.stringify(msg));
term.showOverlay(size.cols + 'x' + size.rows);
}, 500);
});
@@ -286,11 +272,10 @@ export default {
let zsentry = new Zmodem.Sentry({
to_terminal: (octets) => {
this.term.write(Utf8ArrayToStr(octets));
this.term.write(Buffer.from(octets).toString());
},
sender: (octets) => {
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(octets)});
this.ws.send(msg);
this.ws.send(Buffer.from(octets));
},
on_retract: () => {
this.upfile.modal = false;
@@ -309,22 +294,27 @@ export default {
});
this.zsentry = zsentry;
};
ws.on('data', (data) => {
let pbf = new Pbf(data);
let msg = rttyMsg.read(pbf);
ws.onmessage = (ev) => {
let zsentry = this.zsentry;
let term = this.term;
if (msg.type == rttyMsg.Type.LOGINACK.value) {
if (msg.code == rttyMsg.LoginCode.OFFLINE.value) {
this.$Message.error(this.$t('Device offline'));
if (typeof ev.data == 'string') {
let msg = JSON.parse(ev.data);
if (msg.type == "login") {
if (msg.err == 1) {
this.$Message.error(this.$t('Device offline'));
this.logout();
return;
} else if (msg.err == 2) {
this.$Message.error(this.$t('Sessions is full'));
this.logout();
return;
}
this.sid = msg.sid;
msg = rttyMsgInit('WINSIZE', {sid: this.sid, cols: term.cols, rows: term.rows});
ws.send(msg);
msg = {type: 'winsize', sid: this.sid, cols: term.cols, rows: term.rows};
ws.send(JSON.stringify(msg));
term.on('data', (data) => {
let zsession = zsentry.get_confirmed_session();
@@ -338,43 +328,42 @@ export default {
return;
}
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(data)});
ws.send(msg);
this.ws.send(Buffer.from(data));
});
} else if (msg.type == rttyMsg.Type.TTY.value) {
if (!this.recvTTYCnt)
this.recvTTYCnt = 0;
this.recvTTYCnt++;
} else if (msg.type == 'logout') {
this.logout();
}
} else {
if (!this.recvTTYCnt)
this.recvTTYCnt = 0;
this.recvTTYCnt++;
if (this.recvTTYCnt < 4) {
let data = Utf8ArrayToStr(msg.data);
if (this.recvTTYCnt < 4) {
let data = Buffer.from(ev.data).toString();
if (data.match('login:') && this.username && this.username != '') {
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(this.username + '\n')});
ws.send(msg);
return;
}
if (data.match('Password:') && this.password && this.password != '') {
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(this.password + '\n')});
ws.send(msg);
return;
}
if (data.match('login:') && this.username && this.username != '') {
ws.send(Buffer.from(this.username + '\n'));
return;
}
zsentry.consume(msg.data);
if (data.match('Password:') && this.password && this.password != '') {
ws.send(Buffer.from(this.password + '\n'));
return;
}
}
});
});
ws.on('error', () => {
zsentry.consume(ev.data);
}
};
ws.onerror = () => {
this.$Message.error(this.$t('Connect failed'));
this.logout();
});
};
ws.on('close', () => {
ws.onclose = () => {
this.logout();
});
};
}
};
</script>
+2 -1
View File
@@ -33,7 +33,8 @@ const RttyI18n = {
'Login Fail! username or password wrong.': '登录失败用户名或密码错误',
'Connect failed': '连接失败',
'device-count': '在线设备数{count}',
'Cannot be greater than 500MB': '不能大于500MB'
'Cannot be greater than 500MB': '不能大于500MB',
'Sessions is full':'会话已满'
}
}
-43
View File
@@ -1,43 +0,0 @@
/* utf.js - UTF-8 <=> UTF-16 convertion
*
* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
* Version: 1.0
* LastModified: Dec 25 1999
* This library is free. You can redistribute it and/or modify it.
*/
function Utf8ArrayToStr(array) {
var out, i, len, c;
var char2, char3;
out = "";
len = array.length;
i = 0;
while(i < len) {
c = array[i++];
switch(c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
export default Utf8ArrayToStr
+127 -131
View File
@@ -20,186 +20,182 @@
package main
import (
"os"
"fmt"
"sync"
"flag"
"time"
"runtime"
"strconv"
"crypto/md5"
"math/rand"
"net/http"
"encoding/hex"
"encoding/json"
_ "github.com/zhaojh329/rttys/statik"
"github.com/rakyll/statik/fs"
"crypto/md5"
"encoding/hex"
"flag"
"fmt"
"math/rand"
"net/http"
"os"
"runtime"
"strconv"
"sync"
"time"
"github.com/rakyll/statik/fs"
_ "github.com/zhaojh329/rttys/statik"
)
const MAX_SESSION_TIME = 30 * time.Minute
type DeviceInfo struct {
ID string `json:"id"`
Uptime int64 `json:"uptime"`
Description string `json:"description"`
}
type HttpSession struct {
active time.Duration
active time.Duration
}
func allowOrigin(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("content-type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("content-type", "application/json")
}
var hsMutex sync.Mutex
var httpSessions = make(map[string]*HttpSession)
func cleanHttpSession() {
defer hsMutex.Unlock()
defer hsMutex.Unlock()
hsMutex.Lock()
for sid, s := range httpSessions {
s.active = s.active - time.Second
if s.active == 0 {
delete(httpSessions, sid)
}
}
time.AfterFunc(1 * time.Second, cleanHttpSession)
hsMutex.Lock()
for sid, s := range httpSessions {
s.active = s.active - time.Second
if s.active == 0 {
delete(httpSessions, sid)
}
}
time.AfterFunc(1*time.Second, cleanHttpSession)
}
func generateHttpSID(username, password string) string {
md5Ctx := md5.New()
md5Ctx.Write([]byte(username + strconv.FormatFloat(rand.Float64(), 'e', 6, 32) + password))
cipherStr := md5Ctx.Sum(nil)
return hex.EncodeToString(cipherStr)
md5Ctx := md5.New()
md5Ctx.Write([]byte(username + strconv.FormatFloat(rand.Float64(), 'e', 6, 32) + password))
cipherStr := md5Ctx.Sum(nil)
return hex.EncodeToString(cipherStr)
}
func httpAuth(w http.ResponseWriter, r *http.Request) bool {
c, err := r.Cookie("sid")
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
c, err := r.Cookie("sid")
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
defer hsMutex.Unlock()
defer hsMutex.Unlock()
hsMutex.Lock()
hsMutex.Lock()
s, ok := httpSessions[c.Value]
if !ok {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
s, ok := httpSessions[c.Value]
if !ok {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
s.active = MAX_SESSION_TIME
s.active = MAX_SESSION_TIME
return true
return true
}
func main() {
port := flag.Int("port", 5912, "http service port")
cert := flag.String("cert", "", "certFile Path")
key := flag.String("key", "", "keyFile Path")
port := flag.Int("port", 5912, "http service port")
cert := flag.String("cert", "", "certFile Path")
key := flag.String("key", "", "keyFile Path")
if !checkUser() {
rlog.Println("Operation not permitted")
os.Exit(1)
}
if !checkUser() {
rlog.Println("Operation not permitted")
os.Exit(1)
}
flag.Parse()
flag.Parse()
rand.Seed(time.Now().Unix())
rand.Seed(time.Now().Unix())
rlog.Printf("go version: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
rlog.Println("rttys version:", rttys_version())
rlog.Printf("go version: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
rlog.Println("rttys version:", rttys_version())
br := newBroker()
go br.run()
br := newBroker()
go br.run()
statikFS, err := fs.New()
if err != nil {
rlog.Fatal(err)
return
}
statikFS, err := fs.New()
if err != nil {
rlog.Fatal(err)
return
}
staticfs := http.FileServer(statikFS)
staticfs := http.FileServer(statikFS)
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(br, w, r)
})
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(br, w, r)
})
http.HandleFunc("/cmd", func(w http.ResponseWriter, r *http.Request) {
allowOrigin(w)
serveCmd(br, w, r)
})
http.HandleFunc("/cmd", func(w http.ResponseWriter, r *http.Request) {
allowOrigin(w)
serveCmd(br, w, r)
})
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
username := r.PostFormValue("username")
password := r.PostFormValue("password")
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
username := r.PostFormValue("username")
password := r.PostFormValue("password")
if login(username, password) {
sid := generateHttpSID(username, password)
cookie := http.Cookie{
Name: "sid",
Value: sid,
HttpOnly: true,
}
if login(username, password) {
sid := generateHttpSID(username, password)
cookie := http.Cookie{
Name: "sid",
Value: sid,
HttpOnly: true,
}
hsMutex.Lock()
httpSessions[sid] = &HttpSession{
active: MAX_SESSION_TIME,
}
hsMutex.Unlock()
hsMutex.Lock()
httpSessions[sid] = &HttpSession{
active: MAX_SESSION_TIME,
}
hsMutex.Unlock()
w.Header().Set("Set-Cookie", cookie.String())
fmt.Fprint(w, sid)
return
}
w.Header().Set("Set-Cookie", cookie.String())
fmt.Fprint(w, sid)
return
}
http.Error(w, "Forbidden", http.StatusForbidden)
})
http.Error(w, "Forbidden", http.StatusForbidden)
})
http.HandleFunc("/devs", func(w http.ResponseWriter, r *http.Request) {
if !httpAuth(w, r) {
return
}
http.HandleFunc("/devs", func(w http.ResponseWriter, r *http.Request) {
if !httpAuth(w, r) {
return
}
devs := make([]DeviceInfo, 0)
for _, c := range br.devices {
if c.isDev {
d := DeviceInfo{c.devid, time.Now().Unix() - c.timestamp, c.description}
devs = append(devs, d)
}
}
devs := "["
allowOrigin(w)
for _, c := range br.devices {
if c.isDev {
devs += fmt.Sprintf(`{"id":"%s","uptime":%d,"description":"%s"}`,
c.devid, time.Now().Unix()-c.timestamp, c.desc)
}
}
rsp, _ := json.Marshal(devs)
w.Write(rsp)
})
devs += "]"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
t := r.URL.Query().Get("t")
id := r.URL.Query().Get("id")
allowOrigin(w)
if t == "" && id == "" {
http.Redirect(w, r, "/?t=" + strconv.FormatInt(time.Now().Unix(), 10), http.StatusFound)
return
}
}
w.Write([]byte(devs))
})
staticfs.ServeHTTP(w, r)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
t := r.URL.Query().Get("t")
id := r.URL.Query().Get("id")
if *cert != "" && *key != "" {
rlog.Println("Listen on: ", *port, "SSL on")
rlog.Fatal(http.ListenAndServeTLS(":" + strconv.Itoa(*port), *cert, *key, nil))
} else {
rlog.Println("Listen on: ", *port, "SSL off")
rlog.Fatal(http.ListenAndServe(":" + strconv.Itoa(*port), nil))
}
if t == "" && id == "" {
http.Redirect(w, r, "/?t="+strconv.FormatInt(time.Now().Unix(), 10), http.StatusFound)
return
}
}
staticfs.ServeHTTP(w, r)
})
if *cert != "" && *key != "" {
rlog.Println("Listen on: ", *port, "SSL on")
rlog.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(*port), *cert, *key, nil))
} else {
rlog.Println("Listen on: ", *port, "SSL off")
rlog.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
}
}
-317
View File
@@ -1,317 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: rtty.proto
/*
Package rtty is a generated protocol buffer package.
It is generated from these files:
rtty.proto
It has these top-level messages:
RttyMessage
*/
package rtty
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type RttyMessage_Type int32
const (
RttyMessage_UNKNOWN RttyMessage_Type = 0
RttyMessage_LOGIN RttyMessage_Type = 1
RttyMessage_LOGINACK RttyMessage_Type = 2
RttyMessage_LOGOUT RttyMessage_Type = 3
RttyMessage_TTY RttyMessage_Type = 4
RttyMessage_COMMAND RttyMessage_Type = 5
RttyMessage_WINSIZE RttyMessage_Type = 6
)
var RttyMessage_Type_name = map[int32]string{
0: "UNKNOWN",
1: "LOGIN",
2: "LOGINACK",
3: "LOGOUT",
4: "TTY",
5: "COMMAND",
6: "WINSIZE",
}
var RttyMessage_Type_value = map[string]int32{
"UNKNOWN": 0,
"LOGIN": 1,
"LOGINACK": 2,
"LOGOUT": 3,
"TTY": 4,
"COMMAND": 5,
"WINSIZE": 6,
}
func (x RttyMessage_Type) String() string {
return proto.EnumName(RttyMessage_Type_name, int32(x))
}
func (RttyMessage_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} }
type RttyMessage_LoginCode int32
const (
RttyMessage_OK RttyMessage_LoginCode = 0
RttyMessage_OFFLINE RttyMessage_LoginCode = 1
)
var RttyMessage_LoginCode_name = map[int32]string{
0: "OK",
1: "OFFLINE",
}
var RttyMessage_LoginCode_value = map[string]int32{
"OK": 0,
"OFFLINE": 1,
}
func (x RttyMessage_LoginCode) String() string {
return proto.EnumName(RttyMessage_LoginCode_name, int32(x))
}
func (RttyMessage_LoginCode) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 1} }
type RttyMessage_CommandErr int32
const (
RttyMessage_NONE RttyMessage_CommandErr = 0
RttyMessage_TIMEOUT RttyMessage_CommandErr = 1
RttyMessage_NOTFOUND RttyMessage_CommandErr = 2
RttyMessage_READ RttyMessage_CommandErr = 3
RttyMessage_PERMISSION RttyMessage_CommandErr = 4
RttyMessage_SYSCALL RttyMessage_CommandErr = 5
RttyMessage_DEV_OFFLINE RttyMessage_CommandErr = 6
RttyMessage_CMD_REQUIRED RttyMessage_CommandErr = 7
RttyMessage_DEVID_REQUIRED RttyMessage_CommandErr = 8
)
var RttyMessage_CommandErr_name = map[int32]string{
0: "NONE",
1: "TIMEOUT",
2: "NOTFOUND",
3: "READ",
4: "PERMISSION",
5: "SYSCALL",
6: "DEV_OFFLINE",
7: "CMD_REQUIRED",
8: "DEVID_REQUIRED",
}
var RttyMessage_CommandErr_value = map[string]int32{
"NONE": 0,
"TIMEOUT": 1,
"NOTFOUND": 2,
"READ": 3,
"PERMISSION": 4,
"SYSCALL": 5,
"DEV_OFFLINE": 6,
"CMD_REQUIRED": 7,
"DEVID_REQUIRED": 8,
}
func (x RttyMessage_CommandErr) String() string {
return proto.EnumName(RttyMessage_CommandErr_name, int32(x))
}
func (RttyMessage_CommandErr) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 2} }
type RttyMessage struct {
Version uint32 `protobuf:"varint,1,opt,name=version" json:"version,omitempty"`
Type RttyMessage_Type `protobuf:"varint,2,opt,name=type,enum=RttyMessage_Type" json:"type,omitempty"`
Sid string `protobuf:"bytes,3,opt,name=sid" json:"sid,omitempty"`
Code int32 `protobuf:"varint,4,opt,name=code" json:"code,omitempty"`
Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name" json:"name,omitempty"`
Size uint32 `protobuf:"varint,7,opt,name=size" json:"size,omitempty"`
Id uint32 `protobuf:"varint,8,opt,name=id" json:"id,omitempty"`
Err int32 `protobuf:"varint,9,opt,name=err" json:"err,omitempty"`
Username string `protobuf:"bytes,10,opt,name=username" json:"username,omitempty"`
Password string `protobuf:"bytes,11,opt,name=password" json:"password,omitempty"`
StdOut string `protobuf:"bytes,12,opt,name=std_out,json=stdOut" json:"std_out,omitempty"`
StdErr string `protobuf:"bytes,13,opt,name=std_err,json=stdErr" json:"std_err,omitempty"`
Params []string `protobuf:"bytes,14,rep,name=params" json:"params,omitempty"`
Env map[string]string `protobuf:"bytes,15,rep,name=env" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Cols uint32 `protobuf:"varint,16,opt,name=cols" json:"cols,omitempty"`
Rows uint32 `protobuf:"varint,17,opt,name=rows" json:"rows,omitempty"`
}
func (m *RttyMessage) Reset() { *m = RttyMessage{} }
func (m *RttyMessage) String() string { return proto.CompactTextString(m) }
func (*RttyMessage) ProtoMessage() {}
func (*RttyMessage) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *RttyMessage) GetVersion() uint32 {
if m != nil {
return m.Version
}
return 0
}
func (m *RttyMessage) GetType() RttyMessage_Type {
if m != nil {
return m.Type
}
return RttyMessage_UNKNOWN
}
func (m *RttyMessage) GetSid() string {
if m != nil {
return m.Sid
}
return ""
}
func (m *RttyMessage) GetCode() int32 {
if m != nil {
return m.Code
}
return 0
}
func (m *RttyMessage) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
func (m *RttyMessage) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *RttyMessage) GetSize() uint32 {
if m != nil {
return m.Size
}
return 0
}
func (m *RttyMessage) GetId() uint32 {
if m != nil {
return m.Id
}
return 0
}
func (m *RttyMessage) GetErr() int32 {
if m != nil {
return m.Err
}
return 0
}
func (m *RttyMessage) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *RttyMessage) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
func (m *RttyMessage) GetStdOut() string {
if m != nil {
return m.StdOut
}
return ""
}
func (m *RttyMessage) GetStdErr() string {
if m != nil {
return m.StdErr
}
return ""
}
func (m *RttyMessage) GetParams() []string {
if m != nil {
return m.Params
}
return nil
}
func (m *RttyMessage) GetEnv() map[string]string {
if m != nil {
return m.Env
}
return nil
}
func (m *RttyMessage) GetCols() uint32 {
if m != nil {
return m.Cols
}
return 0
}
func (m *RttyMessage) GetRows() uint32 {
if m != nil {
return m.Rows
}
return 0
}
func init() {
proto.RegisterType((*RttyMessage)(nil), "rtty_message")
proto.RegisterEnum("RttyMessage_Type", RttyMessage_Type_name, RttyMessage_Type_value)
proto.RegisterEnum("RttyMessage_LoginCode", RttyMessage_LoginCode_name, RttyMessage_LoginCode_value)
proto.RegisterEnum("RttyMessage_CommandErr", RttyMessage_CommandErr_name, RttyMessage_CommandErr_value)
}
func init() { proto.RegisterFile("rtty.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 521 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x52, 0x4f, 0x6b, 0xdb, 0x30,
0x14, 0xaf, 0xed, 0xd8, 0x89, 0x5f, 0xd2, 0x54, 0x13, 0xa3, 0x13, 0x3d, 0x99, 0x1c, 0x86, 0x4f,
0x3d, 0x74, 0x30, 0xc6, 0x6e, 0xc1, 0x56, 0x8b, 0xa9, 0x23, 0x6d, 0x8a, 0xd3, 0xd2, 0xed, 0x10,
0xbc, 0x59, 0x14, 0xb3, 0xc6, 0x0e, 0x92, 0x93, 0x92, 0x7d, 0x89, 0x7d, 0xd6, 0x7d, 0x83, 0x21,
0x99, 0x94, 0xee, 0xf6, 0xfb, 0xf3, 0xfe, 0xe9, 0x3d, 0x01, 0xa8, 0xae, 0x3b, 0x5c, 0x6e, 0x55,
0xdb, 0xb5, 0xb3, 0xbf, 0x3e, 0x4c, 0x0c, 0x5d, 0x6f, 0xa4, 0xd6, 0xe5, 0xa3, 0xc4, 0x04, 0x86,
0x7b, 0xa9, 0x74, 0xdd, 0x36, 0xc4, 0x89, 0x9c, 0xf8, 0x54, 0x1c, 0x29, 0x7e, 0x0f, 0x83, 0xee,
0xb0, 0x95, 0xc4, 0x8d, 0x9c, 0x78, 0x7a, 0x85, 0x2f, 0x5f, 0xa7, 0x5d, 0x16, 0x87, 0xad, 0x14,
0xd6, 0xc7, 0x08, 0x3c, 0x5d, 0x57, 0xc4, 0x8b, 0x9c, 0x38, 0x14, 0x06, 0x62, 0x0c, 0x83, 0x9f,
0x6d, 0x25, 0xc9, 0x20, 0x72, 0x62, 0x5f, 0x58, 0x6c, 0xb4, 0xaa, 0xec, 0x4a, 0xe2, 0x47, 0x4e,
0x3c, 0x11, 0x16, 0x1b, 0xad, 0x29, 0x37, 0x92, 0x04, 0x36, 0xd5, 0x62, 0xa3, 0xe9, 0xfa, 0xb7,
0x24, 0x43, 0x3b, 0x8c, 0xc5, 0x78, 0x0a, 0x6e, 0x5d, 0x91, 0x91, 0x55, 0xdc, 0xba, 0x32, 0x1d,
0xa5, 0x52, 0x24, 0xb4, 0xe5, 0x0d, 0xc4, 0x17, 0x30, 0xda, 0x69, 0xa9, 0x6c, 0x35, 0xb0, 0xd5,
0x5e, 0xb8, 0xf1, 0xb6, 0xa5, 0xd6, 0xcf, 0xad, 0xaa, 0xc8, 0xb8, 0xf7, 0x8e, 0x1c, 0xbf, 0x83,
0xa1, 0xee, 0xaa, 0x75, 0xbb, 0xeb, 0xc8, 0xc4, 0x5a, 0x81, 0xee, 0x2a, 0xbe, 0xeb, 0x8e, 0x86,
0x69, 0x73, 0xfa, 0x62, 0x50, 0xa5, 0xf0, 0x39, 0x04, 0xdb, 0x52, 0x95, 0x1b, 0x4d, 0xa6, 0x91,
0x67, 0xf4, 0x9e, 0xe1, 0x18, 0x3c, 0xd9, 0xec, 0xc9, 0x59, 0xe4, 0xc5, 0xe3, 0xab, 0xf3, 0xff,
0x97, 0x45, 0x9b, 0x3d, 0x6d, 0x3a, 0x75, 0x10, 0x26, 0xa4, 0xdf, 0xce, 0x93, 0x26, 0xa8, 0x7f,
0xa1, 0xc1, 0x46, 0x53, 0xed, 0xb3, 0x26, 0x6f, 0x7a, 0xcd, 0xe0, 0x8b, 0x8f, 0x30, 0x3a, 0x26,
0x9a, 0x17, 0xff, 0x92, 0x07, 0x7b, 0xa1, 0x50, 0x18, 0x88, 0xdf, 0x82, 0xbf, 0x2f, 0x9f, 0x76,
0xfd, 0x79, 0x42, 0xd1, 0x93, 0xcf, 0xee, 0x27, 0x67, 0xf6, 0x1d, 0x06, 0xe6, 0x3a, 0x78, 0x0c,
0xc3, 0x15, 0xbb, 0x65, 0xfc, 0x9e, 0xa1, 0x13, 0x1c, 0x82, 0x9f, 0xf3, 0x9b, 0x8c, 0x21, 0x07,
0x4f, 0x60, 0x64, 0xe1, 0x3c, 0xb9, 0x45, 0x2e, 0x06, 0x08, 0x72, 0x7e, 0xc3, 0x57, 0x05, 0xf2,
0xf0, 0x10, 0xbc, 0xa2, 0x78, 0x40, 0x03, 0x93, 0x9a, 0xf0, 0xc5, 0x62, 0xce, 0x52, 0xe4, 0x1b,
0x72, 0x9f, 0xb1, 0x65, 0xf6, 0x8d, 0xa2, 0x60, 0x16, 0x41, 0x98, 0xb7, 0x8f, 0x75, 0x93, 0x98,
0x9b, 0x06, 0xe0, 0xf2, 0x5b, 0x74, 0x62, 0x22, 0xf8, 0xf5, 0x75, 0x9e, 0x31, 0x8a, 0x9c, 0xd9,
0x1f, 0x07, 0x20, 0x69, 0x37, 0x9b, 0xb2, 0xb1, 0xfb, 0x1a, 0xc1, 0x80, 0x71, 0x46, 0xfb, 0xa8,
0x22, 0x5b, 0x50, 0xd3, 0xca, 0x0e, 0xc1, 0x78, 0x71, 0xcd, 0x57, 0x2c, 0x45, 0xae, 0x09, 0x12,
0x74, 0x9e, 0x22, 0x0f, 0x4f, 0x01, 0xbe, 0x50, 0xb1, 0xc8, 0x96, 0xcb, 0x8c, 0xb3, 0x7e, 0x92,
0xe5, 0xc3, 0x32, 0x99, 0xe7, 0x39, 0xf2, 0xf1, 0x19, 0x8c, 0x53, 0x7a, 0xb7, 0x3e, 0xf6, 0x0a,
0x30, 0x82, 0x49, 0xb2, 0x48, 0xd7, 0x82, 0x7e, 0x5d, 0x65, 0x82, 0xa6, 0x68, 0x88, 0x31, 0x4c,
0x53, 0x7a, 0x97, 0xbd, 0xd2, 0x46, 0x3f, 0x02, 0xfb, 0xf5, 0x3f, 0xfc, 0x0b, 0x00, 0x00, 0xff,
0xff, 0x4b, 0xb6, 0xc9, 0x16, 0x08, 0x03, 0x00, 0x00,
}
+1 -1
View File
File diff suppressed because one or more lines are too long