Web: Support login

Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
This commit is contained in:
Jianhui Zhao
2018-06-11 01:13:17 +08:00
parent 13334e310a
commit bd84164ec4
12 changed files with 813 additions and 506 deletions
+2
View File
@@ -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
View File
@@ -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>
+84
View File
@@ -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>
+72
View File
@@ -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>
+370
View File
@@ -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>
+26
View File
@@ -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)
});
+27
View File
@@ -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
}
]
})
+8 -1
View File
@@ -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': '连接失败'
}
}
+43
View File
@@ -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
+53
View File
@@ -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)
}
+117 -28
View File
@@ -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
View File
File diff suppressed because one or more lines are too long