mirror of
https://github.com/netfun2000/rttys_zhaojh329.git
synced 2026-02-27 09:53:24 +08:00
Web: Support login
Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
This commit is contained in:
@@ -16,8 +16,10 @@
|
||||
"simple-websocket": "^7.0.2",
|
||||
"string-format-easy": "^1.0.1",
|
||||
"vue": "^2.5.13",
|
||||
"vue-axios": "^2.1.1",
|
||||
"vue-contextmenu-easy": "^1.0.1",
|
||||
"vue-i18n": "^7.8.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"xterm": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+10
-476
@@ -1,485 +1,19 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<Input v-if="!terminal.show" v-model="searchString" icon="search" size="large" @on-change="handleSearch" :placeholder="$t('Please enter the filter key...')" style="width: 400px" />
|
||||
<Table v-if="!terminal.show" :loading="devices.loading" :height="devices.height" :columns="devlistTitle" :data="devices.filtered" style="width: 100%" :no-data-text="$t('No devices connected')"></Table>
|
||||
<div ref="terminal" class="terminal" v-if="terminal.show" @contextmenu="$vuecontextmenu()"></div>
|
||||
<Spin size="large" fix v-if="terminal.loading"></Spin>
|
||||
<VueContextMenu :menulists="menulists" @contentmenu-click="contentmenuClick"></VueContextMenu>
|
||||
<Modal v-model="upfile.modal" width="380" :mask-closable="false" @on-cancel="cancelUpfile">
|
||||
<p slot="header"><span>{{ $t('Upload file to device') }}</span></p>
|
||||
<Upload :before-upload="beforeUpload" action="">
|
||||
<Button type="ghost" icon="Upload">{{ $t('Select the file to upload') }}</Button>
|
||||
</Upload>
|
||||
<Progress v-if="upfile.file !== null" :percent="upfile.percent"></Progress>
|
||||
<div v-if="upfile.file !== null">{{ $t('upfile-info', {name: upfile.file.name}) }}</div>
|
||||
<div slot="footer">
|
||||
<Button type="primary" size="large" long :loading="upfile.loading" @click="doUpload">{{ upfile.loading ? $t('Uploading') : $t('Click to upload') }}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal v-model="downfile.modal" width="700" :mask-closable="false" @on-cancel="cancelDownfile">
|
||||
<p slot="header"><span>{{ $t('Download file from device') }}</span></p>
|
||||
<Input v-if="!downfile.downing" v-model="filterDownFile" icon="search" @on-change="handleFilterDownFile" :placeholder="$t('Please enter the filter key...')">
|
||||
<span slot="prepend">{{ downfile.pathname }}</span>
|
||||
</Input>
|
||||
<Table :loading="downfile.loading" v-if="!downfile.downing" :columns="filelistTitle" height="400" :data="downfile.filelistFiltered" @on-row-dblclick="filelistDblclick"></Table>
|
||||
<Progress v-if="downfile.downing" :percent="downfile.percent"></Progress>
|
||||
<div slot="footer"></div>
|
||||
</Modal>
|
||||
</div>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 axios from 'axios'
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
data() {
|
||||
return {
|
||||
menulists: [
|
||||
{
|
||||
name: 'upfile',
|
||||
caption: this.$t('Upload file to device')
|
||||
},{
|
||||
name: 'downfile',
|
||||
caption: this.$t('Download file from device')
|
||||
},{
|
||||
name: 'increasefontsize',
|
||||
caption: this.$t('Increase font size')
|
||||
},{
|
||||
name: 'decreasefontsize',
|
||||
caption: this.$t('Decrease font size')
|
||||
}
|
||||
],
|
||||
searchString: '',
|
||||
filterDownFile: '',
|
||||
terminal: {loading: false, show: false, term: null, recvCnt: 0},
|
||||
devices: {loading: true, height: document.body.offsetHeight - 20, list: [], filtered: []},
|
||||
upfile: {modal: false, file: null, step: 2048, pos: 0, canceled: false, percent: 0},
|
||||
downfile: {modal: false, loading: true, path: ['/'], pathname: '/', filelist: [], filelistFiltered: [], downing: false, percent: 0},
|
||||
ws: null,
|
||||
sid: '',
|
||||
username: '',
|
||||
password: '',
|
||||
devId: '',
|
||||
devlistTitle: [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
sortType: 'asc',
|
||||
sortable: true
|
||||
}, {
|
||||
title: this.$t('Uptime'),
|
||||
key: 'uptime',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
return h('span', '%t'.format(params.row.uptime));
|
||||
}
|
||||
}, {
|
||||
title: this.$t('Description'),
|
||||
key: 'description'
|
||||
}, {
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (h, params) => {
|
||||
return h('Button', {
|
||||
props: { type: 'primary' },
|
||||
on: {
|
||||
click: () => {
|
||||
this.terminal.loading = true;
|
||||
this.terminal.show = true;
|
||||
this.devId = params.row.id;
|
||||
window.setTimeout(this.login, 200);
|
||||
}
|
||||
}
|
||||
}, this.$t('Connect'));
|
||||
}
|
||||
}
|
||||
],
|
||||
filelistTitle: [
|
||||
{
|
||||
title: this.$t('Name'),
|
||||
key: 'name',
|
||||
render: (h, params) => {
|
||||
if (params.row.dir)
|
||||
return h('div', [
|
||||
h('Icon', {props: {type: 'folder', color: '#FFE793', size: 20}}),
|
||||
h('strong', ' ' + params.row.name)
|
||||
]);
|
||||
else
|
||||
return h('span', params.row.name);
|
||||
}
|
||||
}, {
|
||||
title: this.$t('Size'),
|
||||
key: 'size',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
return h('span', params.row.size && '%1024mB'.format(params.row.size));
|
||||
}
|
||||
}, {
|
||||
title: this.$t('modification'),
|
||||
key: 'mtim',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
if (params.row.mtim)
|
||||
return h('span', new Date(params.row.mtim * 1000).toLocaleString());
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSearch() {
|
||||
this.devices.filtered = this.devices.list.filter(d => {
|
||||
return d.id.indexOf(this.searchString) > -1 || d.description.indexOf(this.searchString) > -1;
|
||||
});
|
||||
},
|
||||
contentmenuClick(name) {
|
||||
let changeFontSize = 0;
|
||||
if (name == 'upfile') {
|
||||
this.upfile = {modal: true, loading: false, file: null, step: 2048, pos: 0, canceled: false, percent: 0};
|
||||
} else if (name == 'downfile') {
|
||||
this.filterDownFile = '';
|
||||
this.downfile = {modal: true, loading: true, path: [], pathname: '/', filelist: [], downing: false, percent: 0};
|
||||
|
||||
let msg = rttyMsgInit('DOWNFILE', {sid: this.sid});
|
||||
this.ws.send(msg);
|
||||
} else if (name == 'increasefontsize') {
|
||||
changeFontSize = 1;
|
||||
} else if (name == 'decreasefontsize') {
|
||||
changeFontSize = -1;
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
let size = this.terminal.term.getOption('fontSize');
|
||||
this.terminal.term.setOption('fontSize', size + changeFontSize);
|
||||
this.terminal.term.fit();
|
||||
this.terminal.term.focus();
|
||||
}, 50);
|
||||
},
|
||||
beforeUpload (file) {
|
||||
this.upfile.file = file;
|
||||
return false;
|
||||
},
|
||||
readFile(fr) {
|
||||
var blob = this.upfile.file.slice(this.upfile.pos, this.upfile.pos + this.upfile.step);
|
||||
fr.readAsArrayBuffer(blob);
|
||||
},
|
||||
doUpload () {
|
||||
if (!this.upfile.file) {
|
||||
this.$Message.error(this.$t('Select the file to upload'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.upfile.loading = true;
|
||||
|
||||
var fr = new FileReader();
|
||||
fr.onload = (e) => {
|
||||
if (this.upfile.canceled)
|
||||
return;
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, code: rttyMsg.FileCode.FILEDATA.value, data: Buffer.from(fr.result)});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.upfile.pos += e.loaded;
|
||||
this.upfile.percent = Math.round(this.upfile.pos / this.upfile.file.size * 100);
|
||||
|
||||
if (this.upfile.pos < this.upfile.file.size) {
|
||||
/* Control the client read speed based on the current buffer and server */
|
||||
if (this.ws.bufferedAmount > this.upfile.pos * 10 || this.ratelimit) {
|
||||
this.ratelimit = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.readFile(fr);
|
||||
}, 100);
|
||||
} else {
|
||||
this.readFile(fr);
|
||||
}
|
||||
} else {
|
||||
this.upfile.modal = false;
|
||||
this.$Message.info(this.$t('Upload success'));
|
||||
}
|
||||
};
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, name: this.upfile.file.name, size: this.upfile.file.size, code: rttyMsg.FileCode.START.value});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.readFile(fr);
|
||||
},
|
||||
cancelUpfile() {
|
||||
if (!this.upfile.loading)
|
||||
return;
|
||||
this.upfile.canceled = true;
|
||||
this.$Message.info(this.$t('Upload canceled'));
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, code: rttyMsg.FileCode.CANCELED.value});
|
||||
this.ws.send(msg);
|
||||
},
|
||||
handleFilterDownFile() {
|
||||
this.downfile.filelistFiltered = this.downfile.filelist.filter(d => {
|
||||
return d.name.indexOf(this.filterDownFile) > -1;
|
||||
});
|
||||
},
|
||||
filelistDblclick(row, index) {
|
||||
let attr = {sid: this.sid};
|
||||
|
||||
this.filterDownFile = '';
|
||||
|
||||
if (row.name == '..') {
|
||||
if (this.downfile.path.length < 1)
|
||||
return;
|
||||
this.downfile.path.pop();
|
||||
} else {
|
||||
this.downfile.path.push(row.name);
|
||||
}
|
||||
|
||||
this.downfile.pathname = '/' + this.downfile.path.join('/');
|
||||
|
||||
if (row.dir) {
|
||||
this.downfile.loading = true;
|
||||
if (!this.downfile.pathname.endsWith('/'))
|
||||
this.downfile.pathname = this.downfile.pathname + '/';
|
||||
} else {
|
||||
this.downfile.received = 0;
|
||||
this.downfile.size = row.size;
|
||||
this.downfile.downing = true;
|
||||
}
|
||||
|
||||
attr.name = this.downfile.pathname;
|
||||
|
||||
let msg = rttyMsgInit('DOWNFILE', attr);
|
||||
this.ws.send(msg);
|
||||
},
|
||||
|
||||
cancelDownfile() {
|
||||
if (this.downfile.downing == true) {
|
||||
let msg = rttyMsgInit('DOWNFILE', {sid: this.sid, code: rttyMsg.FileCode.CANCELED.value});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.$Message.info(this.$t('Download canceled'));
|
||||
}
|
||||
},
|
||||
getQueryString(name) {
|
||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
|
||||
var r = window.location.search.substr(1).match(reg);
|
||||
if (r != null)
|
||||
return unescape(r[2]);
|
||||
return null;
|
||||
},
|
||||
logout(ws, term) {
|
||||
this.terminal.show = false;
|
||||
|
||||
if (ws)
|
||||
ws.destroy();
|
||||
if (term)
|
||||
term.destroy();
|
||||
},
|
||||
login() {
|
||||
let term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 16
|
||||
});
|
||||
term.open(this.$refs['terminal']);
|
||||
term.fit();
|
||||
term.focus();
|
||||
this.terminal.term = term;
|
||||
|
||||
let protocol = 'ws://';
|
||||
if (location.protocol == 'https:')
|
||||
protocol = 'wss://';
|
||||
|
||||
let ws = new Socket(protocol + location.host + '/ws?devid=' + this.devId);
|
||||
ws.on('connect', () => {
|
||||
ws.on('data', (data) => {
|
||||
let pbf = new Pbf(data);
|
||||
let msg = rttyMsg.read(pbf);
|
||||
|
||||
if (msg.type == rttyMsg.Type.LOGINACK.value) {
|
||||
this.terminal.loading = false;
|
||||
|
||||
if (msg.code == rttyMsg.LoginCode.OFFLINE.value) {
|
||||
this.$Message.error(this.$t('Device offline'));
|
||||
this.logout(null, term);
|
||||
return;
|
||||
}
|
||||
this.ws = ws;
|
||||
this.sid = msg.sid;
|
||||
term.on('data', (data) => {
|
||||
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(data)});
|
||||
ws.send(msg);
|
||||
});
|
||||
} else if (msg.type == rttyMsg.Type.TTY.value) {
|
||||
this.terminal.recvCnt++;
|
||||
let data = Utf8ArrayToStr(msg.data);
|
||||
if (this.terminal.recvCnt < 4) {
|
||||
if (data.match('login:') && 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 != '') {
|
||||
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(this.password + '\n')});
|
||||
ws.send(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
term.write(data);
|
||||
} else if (msg.type == rttyMsg.Type.DOWNFILE.value) {
|
||||
let code = msg.code;
|
||||
if (code == rttyMsg.FileCode.START.value) {
|
||||
this.downfile.loading = false;
|
||||
this.downfile.filelist = JSON.parse(Utf8ArrayToStr(msg.data));
|
||||
this.handleFilterDownFile();
|
||||
}
|
||||
else if (code == rttyMsg.FileCode.FILEDATA.value) {
|
||||
if (!this.downfile.data)
|
||||
this.downfile.data = new Blob([msg.data]);
|
||||
else
|
||||
this.downfile.data = new Blob([this.downfile.data, msg.data]);
|
||||
this.downfile.received += msg.data.byteLength;
|
||||
this.downfile.percent = Math.round(this.downfile.received / this.downfile.size * 100);
|
||||
} else if (code == rttyMsg.FileCode.END.value) {
|
||||
let url = URL.createObjectURL(this.downfile.data);
|
||||
let a = document.createElement('a');
|
||||
a.download = this.downfile.pathname;
|
||||
a.href = url;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.downfile.modal = false;
|
||||
this.downfile.downing = false;
|
||||
this.$Message.info(this.$t('Download Finish'));
|
||||
}
|
||||
} else if (msg.type == rttyMsg.Type.UPFILE.value) {
|
||||
if (msg.code == rttyMsg.FileCode.RATELIMIT.value) {
|
||||
/* Need reduce the sending rate */
|
||||
this.ratelimit = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', ()=> {
|
||||
this.logout(null, term);
|
||||
});
|
||||
|
||||
ws.on('close', ()=> {
|
||||
this.logout(null, term);
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var devId = this.getQueryString('id');
|
||||
var username = this.getQueryString('username');
|
||||
var password = this.getQueryString('password');
|
||||
|
||||
if (username)
|
||||
this.username = username;
|
||||
if (password)
|
||||
this.password = password;
|
||||
|
||||
if (devId) {
|
||||
this.terminal.loading = true;
|
||||
this.terminal.show = true;
|
||||
this.devId = devId;
|
||||
window.setTimeout(this.login, 200);
|
||||
}
|
||||
|
||||
window.setInterval(() => {
|
||||
if (this.terminal.show)
|
||||
return;
|
||||
axios.get('/devs').then(res => {
|
||||
this.devices.loading = false;
|
||||
this.devices.list = res.data;
|
||||
this.handleSearch();
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
this.devices.height = document.body.offsetHeight - 20;
|
||||
if (this.terminal.show) {
|
||||
this.terminal.term.fit();
|
||||
}
|
||||
});
|
||||
}
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 99%;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 100%;
|
||||
margin-left: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #555;
|
||||
}
|
||||
</style>
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div id="home">
|
||||
<Input v-model="filterString" icon="search" size="large" @on-change="handleSearch" :placeholder="$t('Please enter the filter key...')" style="width: 400px" />
|
||||
<Table :loading="loading" :columns="devlistTitle" :data="filtered" style="margin-top: 10px; width: 100%" :no-data-text="$t('No devices connected')"></Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
filterString: '',
|
||||
loading: true,
|
||||
devlists: [],
|
||||
filtered: [],
|
||||
devlistTitle: [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
sortType: 'asc',
|
||||
sortable: true
|
||||
}, {
|
||||
title: this.$t('Uptime'),
|
||||
key: 'uptime',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
return h('span', '%t'.format(params.row.uptime));
|
||||
}
|
||||
}, {
|
||||
title: this.$t('Description'),
|
||||
key: 'description'
|
||||
}, {
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (h, params) => {
|
||||
return h('Button', {
|
||||
props: { type: 'primary' },
|
||||
on: {
|
||||
click: () => {
|
||||
this.$router.push({path: '/rtty', query: {devid: params.row.id}});
|
||||
}
|
||||
}
|
||||
}, this.$t('Connect'));
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSearch() {
|
||||
this.filtered = this.devlists.filter(d => {
|
||||
return d.id.indexOf(this.filterString) > -1 || d.description.indexOf(this.filterString) > -1;
|
||||
});
|
||||
},
|
||||
getDevices() {
|
||||
this.$http.get('/devs').then(res => {
|
||||
this.loading = false;
|
||||
this.devlists = res.data;
|
||||
this.handleSearch();
|
||||
}).catch(err => {
|
||||
this.$router.push('/login');
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$root.$data.interval)
|
||||
clearInterval(this.$root.$data.interval);
|
||||
|
||||
this.$root.$data.interval = setInterval(()=> {
|
||||
this.getDevices();
|
||||
}, 3000);
|
||||
|
||||
this.getDevices();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#home {
|
||||
padding:10px;
|
||||
}
|
||||
</style>
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div @keydown.enter="handleSubmit">
|
||||
<Card class="login-container">
|
||||
<p slot="title">{{ $t('Authorization Required') }}</p>
|
||||
<Form ref="form" :model="form" :rules="ruleValidate">
|
||||
<FormItem prop="username">
|
||||
<Input type="text" v-model="form.username" size="large" auto-complete="off" :placeholder="$t('Enter username...')">
|
||||
<Icon type="ios-person-outline" slot="prepend"></Icon>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Input type="password" v-model="form.password" size="large" auto-complete="off" :placeholder="$t('Enter password...')">
|
||||
<Icon type="ios-locked-outline" slot="prepend"></Icon>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" long size="large" icon="log-in" @click="handleSubmit">{{ $t('Login') }}</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
ruleValidate: {
|
||||
username: [
|
||||
{required: true, trigger: 'blur', message: this.$t('username is required')}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('username', this.form.username);
|
||||
params.append('password', this.form.password);
|
||||
this.$http.post('/login', params).then(res => {
|
||||
sessionStorage.setItem('rtty-sid', res)
|
||||
this.$router.push('/');
|
||||
}).catch(err => {
|
||||
this.$Message.error(this.$t('Login Fail! username or password wrong.'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.login-container {
|
||||
width: 400px;
|
||||
height: 240px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -200px;
|
||||
margin-top: -120px;
|
||||
}
|
||||
</style>
|
||||
Executable
+370
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div id="rtty">
|
||||
<div ref="terminal" class="terminal" @contextmenu="$vuecontextmenu()"></div>
|
||||
<VueContextMenu :menulists="menulists" @contentmenu-click="contentmenuClick"></VueContextMenu>
|
||||
<Modal v-model="upfile.modal" width="380" :mask-closable="false" @on-cancel="cancelUpfile">
|
||||
<p slot="header"><span>{{ $t('Upload file to device') }}</span></p>
|
||||
<Upload v-if="!upfile.loading" :before-upload="beforeUpload" action="">
|
||||
<Button type="ghost" icon="Upload">{{ $t('Select the file to upload') }}</Button>
|
||||
</Upload>
|
||||
<Progress v-if="upfile.loading" :percent="upfile.percent"></Progress>
|
||||
<div v-if="upfile.file !== null">{{ $t('upfile-info', {name: upfile.file.name}) }}</div>
|
||||
<div slot="footer">
|
||||
<Button type="primary" size="large" long :loading="upfile.loading"
|
||||
@click="doUpload">{{ upfile.loading ? $t('Uploading') : $t('Click to upload') }}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal v-model="downfile.modal" width="700" :mask-closable="false" @on-cancel="cancelDownfile">
|
||||
<p slot="header"><span>{{ $t('Download file from device') }}</span></p>
|
||||
<Input v-if="!downfile.downing" v-model="downfile.filter" icon="search"
|
||||
@on-change="handleFilterDownFile" :placeholder="$t('Please enter the filter key...')">
|
||||
<span slot="prepend">{{ downfile.pathname }}</span>
|
||||
</Input>
|
||||
<Table :loading="downfile.loading" v-if="!downfile.downing" :columns="filelistTitle" height="400" :data="downfile.filelistFiltered" @on-row-dblclick="filelistDblclick"></Table>
|
||||
<Progress v-if="downfile.downing" :percent="downfile.percent"></Progress>
|
||||
<div slot="footer"></div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 Utf8ArrayToStr from '@/utf8array_str'
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
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() {
|
||||
return {
|
||||
menulists: [
|
||||
{
|
||||
name: 'upfile',
|
||||
caption: this.$t('Upload file to device')
|
||||
},{
|
||||
name: 'downfile',
|
||||
caption: this.$t('Download file from device')
|
||||
},{
|
||||
name: 'increasefontsize',
|
||||
caption: this.$t('Increase font size')
|
||||
},{
|
||||
name: 'decreasefontsize',
|
||||
caption: this.$t('Decrease font size')
|
||||
}
|
||||
],
|
||||
filelistTitle: [
|
||||
{
|
||||
title: this.$t('Name'),
|
||||
key: 'name',
|
||||
render: (h, params) => {
|
||||
if (params.row.dir)
|
||||
return h('div', [
|
||||
h('Icon', {props: {type: 'folder', color: '#FFE793', size: 20}}),
|
||||
h('strong', ' ' + params.row.name)
|
||||
]);
|
||||
else
|
||||
return h('span', params.row.name);
|
||||
}
|
||||
}, {
|
||||
title: this.$t('Size'),
|
||||
key: 'size',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
return h('span', params.row.size && '%1024mB'.format(params.row.size));
|
||||
}
|
||||
}, {
|
||||
title: this.$t('modification'),
|
||||
key: 'mtim',
|
||||
sortable: true,
|
||||
render: (h, params) => {
|
||||
if (params.row.mtim)
|
||||
return h('span', new Date(params.row.mtim * 1000).toLocaleString());
|
||||
}
|
||||
}
|
||||
],
|
||||
upfile: {modal: false, file: null, step: 2048, pos: 0, percent: 0, loading: false},
|
||||
downfile: {modal: false, loading: true, pathname: '/', filelist: [], filelistFiltered: [], downing: false, percent: 0, filter: ''},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
if (this.ws) {
|
||||
this.ws.destroy();
|
||||
delete this.ws;
|
||||
}
|
||||
|
||||
if (this.term) {
|
||||
this.term.destroy();
|
||||
delete this.term;
|
||||
}
|
||||
|
||||
this.$router.push('/');
|
||||
},
|
||||
contentmenuClick(name) {
|
||||
let changeFontSize = 0;
|
||||
|
||||
if (!this.term)
|
||||
return;
|
||||
|
||||
if (name == 'upfile') {
|
||||
this.upfile = {modal: true, file: null, step: 2048, pos: 0, percent: 0, loading: false};
|
||||
} else if (name == 'downfile') {
|
||||
this.downfile = {modal: true, loading: true, path: [], pathname: '/', filelist: [], downing: false, percent: 0, filter: ''};
|
||||
let msg = rttyMsgInit('DOWNFILE', {sid: this.sid});
|
||||
this.ws.send(msg);
|
||||
} else if (name == 'increasefontsize') {
|
||||
changeFontSize = 1;
|
||||
} else if (name == 'decreasefontsize') {
|
||||
changeFontSize = -1;
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
let size = this.term.getOption('fontSize');
|
||||
|
||||
this.term.setOption('fontSize', size + changeFontSize);
|
||||
this.term.fit();
|
||||
this.term.focus();
|
||||
this.term.refresh();
|
||||
}, 50);
|
||||
},
|
||||
beforeUpload (file) {
|
||||
this.upfile.file = file;
|
||||
this.upfile.lf = true;
|
||||
return false;
|
||||
},
|
||||
readFile(fr) {
|
||||
var blob = this.upfile.file.slice(this.upfile.pos, this.upfile.pos + this.upfile.step);
|
||||
fr.readAsArrayBuffer(blob);
|
||||
},
|
||||
cancelUpfile() {
|
||||
if (!this.upfile.loading)
|
||||
return;
|
||||
this.upfile.canceled = true;
|
||||
this.$Message.info(this.$t('Upload canceled'));
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, code: rttyMsg.FileCode.CANCELED.value});
|
||||
this.ws.send(msg);
|
||||
},
|
||||
doUpload () {
|
||||
if (!this.upfile.file) {
|
||||
this.$Message.error(this.$t('Select the file to upload'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.upfile.loading = true;
|
||||
|
||||
var fr = new FileReader();
|
||||
fr.onload = (e) => {
|
||||
if (this.upfile.canceled)
|
||||
return;
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, code: rttyMsg.FileCode.FILEDATA.value, data: Buffer.from(fr.result)});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.upfile.pos += e.loaded;
|
||||
this.upfile.percent = Math.round(this.upfile.pos / this.upfile.file.size * 100);
|
||||
|
||||
if (this.upfile.pos < this.upfile.file.size) {
|
||||
/* Control the client read speed based on the current buffer and server */
|
||||
if (this.ws.bufferedAmount > this.upfile.pos * 10 || this.upfile.ratelimit) {
|
||||
this.upfile.ratelimit = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.readFile(fr);
|
||||
}, 100);
|
||||
} else {
|
||||
this.readFile(fr);
|
||||
}
|
||||
} else {
|
||||
this.upfile.modal = false;
|
||||
this.$Message.info(this.$t('Upload success'));
|
||||
}
|
||||
};
|
||||
|
||||
let msg = rttyMsgInit('UPFILE', {sid: this.sid, name: this.upfile.file.name, size: this.upfile.file.size, code: rttyMsg.FileCode.START.value});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.readFile(fr);
|
||||
},
|
||||
cancelDownfile() {
|
||||
if (this.downfile.downing) {
|
||||
let msg = rttyMsgInit('DOWNFILE', {sid: this.sid, code: rttyMsg.FileCode.CANCELED.value});
|
||||
this.ws.send(msg);
|
||||
|
||||
this.$Message.info(this.$t('Download canceled'));
|
||||
}
|
||||
},
|
||||
handleFilterDownFile() {
|
||||
this.downfile.filelistFiltered = this.downfile.filelist.filter(d => {
|
||||
return d.name.indexOf(this.downfile.filter) > -1;
|
||||
});
|
||||
},
|
||||
filelistDblclick(row, index) {
|
||||
let attr = {sid: this.sid};
|
||||
|
||||
this.downfile.filter = '';
|
||||
|
||||
if (row.name == '..') {
|
||||
if (this.downfile.path.length < 1)
|
||||
return;
|
||||
this.downfile.path.pop();
|
||||
} else {
|
||||
this.downfile.path.push(row.name);
|
||||
}
|
||||
|
||||
this.downfile.pathname = '/' + this.downfile.path.join('/');
|
||||
|
||||
if (row.dir) {
|
||||
this.downfile.loading = true;
|
||||
if (!this.downfile.pathname.endsWith('/'))
|
||||
this.downfile.pathname = this.downfile.pathname + '/';
|
||||
} else {
|
||||
this.downfile.received = 0;
|
||||
this.downfile.size = row.size;
|
||||
this.downfile.downing = true;
|
||||
}
|
||||
|
||||
attr.name = this.downfile.pathname;
|
||||
|
||||
let msg = rttyMsgInit('DOWNFILE', attr);
|
||||
this.ws.send(msg);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let devid = this.$route.query.devid;
|
||||
let protocol = 'ws://';
|
||||
|
||||
this.username = this.$route.query.username;
|
||||
this.password = this.$route.query.password;
|
||||
|
||||
if (location.protocol == 'https:')
|
||||
protocol = 'wss://';
|
||||
|
||||
let ws = new Socket(protocol + location.host + '/ws?devid=' + devid);
|
||||
this.ws = ws;
|
||||
|
||||
ws.on('connect', () => {
|
||||
let term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 16
|
||||
});
|
||||
|
||||
term.open(this.$refs['terminal']);
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
this.term = term;
|
||||
|
||||
ws.on('data', (data) => {
|
||||
let pbf = new Pbf(data);
|
||||
let msg = rttyMsg.read(pbf);
|
||||
|
||||
if (msg.type == rttyMsg.Type.LOGINACK.value) {
|
||||
if (msg.code == rttyMsg.LoginCode.OFFLINE.value) {
|
||||
this.$Message.error(this.$t('Device offline'));
|
||||
this.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
this.sid = msg.sid;
|
||||
|
||||
term.on('data', (data) => {
|
||||
let msg = rttyMsgInit('TTY', {sid: this.sid, data: Buffer.from(data)});
|
||||
ws.send(msg);
|
||||
});
|
||||
} else if (msg.type == rttyMsg.Type.TTY.value) {
|
||||
let data = Utf8ArrayToStr(msg.data);
|
||||
|
||||
if (!this.recvTTYCnt)
|
||||
this.recvTTYCnt = 0;
|
||||
this.recvTTYCnt++;
|
||||
|
||||
if (this.recvTTYCnt < 4) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
term.write(data);
|
||||
} else if (msg.type == rttyMsg.Type.UPFILE.value) {
|
||||
if (msg.code == rttyMsg.FileCode.RATELIMIT.value) {
|
||||
/* Need reduce the sending rate */
|
||||
this.upfile.ratelimit = true;
|
||||
}
|
||||
} else if (msg.type == rttyMsg.Type.DOWNFILE.value) {
|
||||
let code = msg.code;
|
||||
if (code == rttyMsg.FileCode.START.value) {
|
||||
this.downfile.loading = false;
|
||||
this.downfile.filelist = JSON.parse(Utf8ArrayToStr(msg.data));
|
||||
this.handleFilterDownFile();
|
||||
}
|
||||
else if (code == rttyMsg.FileCode.FILEDATA.value) {
|
||||
if (!this.downfile.data)
|
||||
this.downfile.data = new Blob([msg.data]);
|
||||
else
|
||||
this.downfile.data = new Blob([this.downfile.data, msg.data]);
|
||||
this.downfile.received += msg.data.byteLength;
|
||||
this.downfile.percent = Math.round(this.downfile.received / this.downfile.size * 100);
|
||||
} else if (code == rttyMsg.FileCode.END.value) {
|
||||
let url = URL.createObjectURL(this.downfile.data);
|
||||
let a = document.createElement('a');
|
||||
a.download = this.downfile.pathname;
|
||||
a.href = url;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.downfile.modal = false;
|
||||
this.downfile.downing = false;
|
||||
this.$Message.info(this.$t('Download Finish'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
this.$Message.error(this.$t('Connect failed'));
|
||||
this.logout();
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
this.logout();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#rtty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,9 @@ import enLocale from 'iview/dist/locale/en-US'
|
||||
import 'string-format-easy'
|
||||
import VueContextMenu from 'vue-contextmenu-easy'
|
||||
import RttyI18n from './rtty-i18n'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import VueAxios from 'vue-axios'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@@ -18,6 +21,8 @@ Vue.use(iView);
|
||||
|
||||
Vue.use(VueContextMenu);
|
||||
|
||||
Vue.use(VueAxios, axios)
|
||||
|
||||
const messages = {
|
||||
'zh-CN': Object.assign(zhLocale, RttyI18n['zh-CN']),
|
||||
'en-US': Object.assign(enLocale, RttyI18n['en-US'])
|
||||
@@ -33,9 +38,30 @@ const i18n = new VueI18n({
|
||||
messages: messages
|
||||
});
|
||||
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.path == '/rtty' && to.query.devid) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.path == '/' && to.query.id) {
|
||||
router.push({path: '/rtty', query: {devid: to.query.id, username: to.query.username, password: to.query.password}});
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.path != '/login' && !sessionStorage.getItem('rtty-sid')) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
i18n: i18n,
|
||||
el: '#app',
|
||||
router,
|
||||
render: (h)=>h(App)
|
||||
});
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Login from '@/components/Login'
|
||||
import Home from '@/components/Home'
|
||||
import Rtty from '@/components/Rtty'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/rtty',
|
||||
name: 'Rtty',
|
||||
component: Rtty
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -23,7 +23,14 @@ const RttyI18n = {
|
||||
'modification': '修改时间',
|
||||
'upfile-info': '文件"{name}"将会保存到你的设备的"/tmp"目录',
|
||||
'Name': '名称',
|
||||
'Size': '大小'
|
||||
'Size': '大小',
|
||||
'Authorization Required': '需要授权',
|
||||
'Enter username...': '请输入用户名...',
|
||||
'Enter password...': '请输入密码...',
|
||||
'Login': '登录',
|
||||
'username is required': '用户名为必填',
|
||||
'Login Fail! username or password wrong.': '登录失败,用户名或密码错误',
|
||||
'Connect failed': '连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
/* 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
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -D_GNU_SOURCE=1
|
||||
#cgo LDFLAGS: -lcrypt
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <shadow.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <crypt.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
static bool login(const char *username, const char *password)
|
||||
{
|
||||
struct spwd spw;
|
||||
struct spwd *result;
|
||||
struct crypt_data cdata;
|
||||
char buf[1024], *sp;
|
||||
int s;
|
||||
|
||||
if (!username || *username == 0)
|
||||
return false;
|
||||
|
||||
s = getspnam_r(username, &spw, buf, sizeof(buf), &result);
|
||||
if (s || !result)
|
||||
return false;
|
||||
|
||||
cdata.initialized = 0;
|
||||
sp = crypt_r(password, spw.sp_pwdp, &cdata);
|
||||
if (!sp)
|
||||
return false;
|
||||
|
||||
return !strcmp(sp, spw.sp_pwdp);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func login(username, password string) bool {
|
||||
c_username := C.CString(username)
|
||||
c_password := C.CString(password)
|
||||
|
||||
ok := C.login(c_username, c_password);
|
||||
|
||||
C.free(unsafe.Pointer(c_username))
|
||||
C.free(unsafe.Pointer(c_password))
|
||||
|
||||
return bool(ok)
|
||||
}
|
||||
@@ -20,34 +20,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"flag"
|
||||
"time"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"crypto/md5"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
_ "github.com/zhaojh329/rttys/statik"
|
||||
"github.com/rakyll/statik/fs"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
var hsMutex sync.Mutex
|
||||
var httpSessions = make(map[string]*HttpSession)
|
||||
|
||||
func cleanHttpSession() {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
defer hsMutex.Unlock()
|
||||
|
||||
hsMutex.Lock()
|
||||
|
||||
s, ok := httpSessions[c.Value]
|
||||
if !ok {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
|
||||
s.active = MAX_SESSION_TIME
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 5912, "http service port")
|
||||
cert := flag.String("cert", "", "certFile Path")
|
||||
key := flag.String("key", "", "keyFile Path")
|
||||
|
||||
if syscall.Getuid() != 0 {
|
||||
log.Println("Operation not permitted")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
@@ -65,6 +127,60 @@ func main() {
|
||||
|
||||
staticfs := http.FileServer(statikFS)
|
||||
|
||||
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("/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,
|
||||
}
|
||||
|
||||
hsMutex.Lock()
|
||||
httpSessions[sid] = &HttpSession{
|
||||
active: MAX_SESSION_TIME,
|
||||
}
|
||||
hsMutex.Unlock()
|
||||
|
||||
w.Header().Set("Set-Cookie", cookie.String())
|
||||
fmt.Fprint(w, sid)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
allowOrigin(w)
|
||||
|
||||
rsp, _ := json.Marshal(devs)
|
||||
w.Write(rsp)
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
t := r.URL.Query().Get("t")
|
||||
@@ -76,33 +192,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if r.URL.Path == "/devs" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
allowOrigin(w)
|
||||
|
||||
rsp, _ := json.Marshal(devs)
|
||||
w.Write(rsp)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/cmd" {
|
||||
allowOrigin(w)
|
||||
serveCmd(br, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/ws" {
|
||||
serveWs(br, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
staticfs.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user