first commit

This commit is contained in:
Sam Sam
2022-07-06 14:15:00 +08:00
commit 3b85a5affd
26 changed files with 7557 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
**/speedtest-static/*
**/ip.mmdb
**/.DS_Store
**/node_modules
**/package-lock.json

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:lts-alpine
ADD ui /app
WORKDIR /app
RUN npm i && \
npm run build
FROM alpine:3.16
LABEL maintainer="samlm0 <i@teddysun.com>"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
apk add --no-cache php81 php81-pecl-maxminddb php81-ctype php81-pecl-swoole nginx xz \
iperf iperf3 \
mtr \
traceroute \
iputils
ADD backend/app /app
COPY --from=0 /app/dist /app/webspaces
CMD php81 /app/app.php

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# ALS - Another Looking-glass Server
## Quick start
```
docker run -d --restart always --network host wikihostinc/looking-glass-server
```
## Host Requirements
- Can run docker (yes, only docker is required)
## Image Environment Variables
| Key | Example | Default | Description |
| ---- | ---- | ---- | ---- |
| HTTP_PORT | 80 | 80 | which HTTP port should use |
| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | size of static test files, separate with space |
| PUBLIC_IPV4 | 1.1.1.1 | (fetch from http://ifconfig.co) | The IPv4 address of the server |
| PUBLIC_IPV6 | fe80::1 | (fetch from http://ifconfig.co) | The IPv6 address of the server|
| DISPLAY_TRAFFIC | true | true | Toggle the streaming traffic graph |
| ENABLE_SPEEDTEST | true | true | Toggle the streaming traffic graph |
| UTILITIES_PING | true | true | Toggle the ping feature |
| UTILITIES_TRACEROUTE | true | true | Toggle the traceroute feature |
| UTILITIES_IPERF3 | true | true | Toggle the iperf3 feature |
| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iperf3 listen port range - from |
| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iperf3 listen port range - to |
## Features
- [x] HTML 5 Speed Test
- [x] Traceroute - IPv4
- [x] Ping - IPv4
- [x] iPerf3 server
- [x] Streaming traffic graph
## TO-DO List
- [ ] Traceroute - IPv6
- [ ] Ping - IPv6
## License
Code is licensed under MIT Public License.
* If you wish to support my efforts, keep the "Powered by LookingGlass" link intact.

14
ui/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
"root": true,
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-prettier"
],
"env": {
"vue/setup-compiler-macros": true
}
}

28
ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
ui/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

35
ui/README.md Normal file
View File

@@ -0,0 +1,35 @@
# ui
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

8
ui/build.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
BASEDIR=$(dirname "$0")
cd $BASEDIR
npm run build
rm -rf ../scripts/app/webspaces/assets
rm -rf ../scripts/app/webspaces/{index.html,favicon.ico,speedtest_worker.min.js}
cp -r dist/* ../scripts/app/webspaces/

16
ui/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Looking glass server</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

6241
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
ui/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"apexcharts": "^3.35.3",
"vue": "^3.2.36",
"vue3-apexcharts": "^1.4.1",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.0.0",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^8.2.0",
"naive-ui": "^2.30.4",
"prettier": "^2.5.1",
"unplugin-vue-components": "^0.19.6",
"vfonts": "^0.0.3",
"vite": "^2.9.9"
}
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
ui/public/speedtest_worker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

96
ui/src/App.vue Normal file
View File

@@ -0,0 +1,96 @@
<template>
<n-config-provider :theme="darkTheme">
<Loading v-show="!isLoaded"></Loading>
<div v-show="isLoaded">
<n-space vertical>
<h2>Looking Glass Server</h2>
<Information v-model:wsMessage="wsMessage" v-model:componentConfig="componentConfig"></Information>
<Utilities v-model:componentConfig="componentConfig" v-model:ws="ws" v-model:wsMessage="wsMessage"></Utilities>
<Speedtest v-model:componentConfig="componentConfig" v-show="componentConfig.display_speedtest"></Speedtest>
<TrafficDisplay v-show="componentConfig.display_traffic" v-model:wsMessage="wsMessage"></TrafficDisplay>
</n-space>
</div>
</n-config-provider>
</template>
<script>
import Loading from './components/Loading.vue'
import { defineComponent, defineAsyncComponent, reactive } from 'vue'
import { darkTheme } from 'naive-ui'
export default defineComponent({
components: {
Information: defineAsyncComponent(() => import('./components/Information.vue')),
TrafficDisplay: defineAsyncComponent(() => import('./components/TrafficDisplay.vue')),
Speedtest: defineAsyncComponent(() => import('./components/Speedtest.vue')),
Utilities: defineAsyncComponent(() => import('./components/Utilities.vue')),
},
created() {
this.initWebsocket();
},
methods: {
initWebsocket() {
this.ws = new WebSocket(location.protocol.replace('http', 'ws') + '//' + location.host + '/ws')
this.ws.onopen = () => {
this.isLoaded = true
}
this.ws.onmessage = (message) => {
this.wsMessage.push(message.data.split('|'))
};
this.ws.onclose = () => {
this.isLoaded = false
setTimeout(() => {
this.initWebsocket()
}, 1000)
}
this.ws.onerror = () => {
this.isLoaded = false
setTimeout(() => {
this.initWebsocket()
}, 1000)
}
}
},
data() {
return {
ws: false,
wsMessage: reactive([]),
isLoaded: false,
componentConfig: reactive({})
}
},
setup() {
return {
darkTheme
}
}
})
</script>
<style>
@import './assets/base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
/* @media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 0fr 0fr;
padding: 0 2rem;
}
.loading-screen {
min-width: 500px;
}
} */
</style>

49
ui/src/assets/base.css Normal file
View File

@@ -0,0 +1,49 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
ui/src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1,54 @@
<template>
<div>
<n-card title="Server Information" hoverable>
<n-space vertical>
<div v-show="location">
服务器地点: {{ location }}
</div>
<div v-show="publicIpv4">
公网 IPv4 地址: <n-tag>{{ publicIpv4 }}</n-tag>
</div>
<div v-show="publicIpv6">
公网 IPv6 地址: <n-tag> [{{ publicIpv6 }}]</n-tag>
</div>
</n-space>
<!-- <n-progress type=" line" :percentage="100" :show-indicator="false" processing /> -->
</n-card>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: {
wsMessage: Array,
componentConfig: Object
},
data() {
return {
location: false,
publicIpv4: false,
publicIpv6: false,
}
},
mounted() {
let DataWatcher = this.$watch(() => this.wsMessage, () => {
this.wsMessage.forEach((e, i) => {
if (e[0] != 1000) return;
let data = JSON.parse(e[1])
this.wsMessage.splice(i, 1)
this.location = data.location
this.publicIpv4 = data.public_ipv4
this.publicIpv6 = data.public_ipv6
this.componentConfig.public_ipv4 = data.public_ipv4
this.componentConfig.public_ipv6 = data.public_ipv6
this.componentConfig.testFiles = data.testfiles
this.componentConfig.display_traffic = data.display_traffic
this.componentConfig.display_speedtest = data.display_speedtest
DataWatcher()
})
}, { immediate: true, deep: true });
}
})
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div class="loading-screen">
<n-card title="Loading...">
<n-progress type="line" :percentage="100" :show-indicator="false" processing />
</n-card>
</div>
</template>
<script>
export {
}
</script>

View File

@@ -0,0 +1,261 @@
<template>
<n-card title="Server Speedtest">
<div>
<n-card title="HTML 5 在线测速" style="margin-bottom: 10px;">
<div v-show="h5Download !== '...'">
<n-grid x-gap="12" cols="1 s:1 m:1 l:3" responsive="screen">
<n-gi span="1 s:1 m:1 l:2">
<div>
<h4>下行</h4>
<h1>{{ h5Download }} Mbps</h1>
<apexchart type="line" :options="h5SpeedtestDownloadSpeedChart.chartOptions"
height="200px" :series="h5SpeedtestDownloadSpeedChart.series">
</apexchart>
</div>
</n-gi>
<n-gi span="1">
<div>
<h4>上行</h4>
<h1>{{ h5Upload }} Mbps</h1>
<apexchart type="line" :options="h5SpeedtestUploadSpeedChart.chartOptions"
height="200px" :series="h5SpeedtestUploadSpeedChart.series">
</apexchart>
</div>
</n-gi>
</n-grid>
</div>
<!-- <h3>下行:
<n-number-animation :to="h5Download" />
</h3>
<h3>上行: {{ h5Upload }}</h3> -->
<n-button size="large" @click="startOrStopSpeedtest" style="margin-top: 10px;">
<n-spin size="small" v-show="h5SpeedWorker !== null" style="margin-right: 10px;" /> {{
h5SpeedtestButtonText
}}
</n-button>
</n-card>
</div>
<div v-show="componentConfig?.testFiles?.length > 0" style="margin-bottom: 10px;">
<n-card title="静态文件测速">
<div v-for="i in componentConfig.testFiles">
<n-button text tag="a" :href="`speedtest-static/${i}.test`" target="_blank" type="primary">
{{ i }}
</n-button>
</div>
</n-card>
</div>
</n-card>
</template>
<script>
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
components: {
apexchart: defineAsyncComponent(() => import('vue3-apexcharts')),
},
props: {
componentConfig: Object
},
methods: {
startOrStopSpeedtest(force = true) {
if (this.h5SpeedWorker !== null) {
this.h5SpeedWorker.postMessage('abort')
clearInterval(this.h5SpeedWorkerTimer)
this.h5SpeedtestButtonText = '开始测速'
this.h5SpeedWorker = null
if (force) {
this.h5Upload = '...'
this.h5Download = '...'
}
return;
}
this.h5Upload = '...'
this.h5Download = '...'
this.h5SpeedtestButtonText = '停止测速'
this.h5SpeedWorker = new Worker('speedtest_worker.min.js')
// this
this.h5SpeedWorker.onmessage = (e) => {
var nowPointName = (new Date()).getHours().toString().padStart(2, '0') + ':' +
(new Date()).getMinutes().toString().padStart(2, '0') + ':' +
(new Date()).getSeconds().toString().padStart(2, '0')
var data = JSON.parse(e.data);
var status = data.testState;
if (status >= 4) {
return this.startOrStopSpeedtest(false)
}
if (status == 1 && data.dlStatus == 0) {
this.h5Download = '...'
} else {
if (data.dlStatus) {
if (data.dlStatus != this.h5Download) {
this.h5Download = data.dlStatus
let ChartData = this.h5SpeedtestDownloadSpeedChart.series[0].data
let categories = this.h5SpeedtestDownloadSpeedChart.chartOptions.xaxis.categories
ChartData.push(this.h5Download)
categories.push(nowPointName)
this.h5SpeedtestDownloadSpeedChart.chartOptions.value = {
xaxis: { categories: categories }
}
}
}
}
if (status == 1 && data.ulStatus == 0) {
this.h5Upload = '...'
} else {
if (data.ulStatus) {
if (data.ulStatus != this.h5Upload) {
this.h5Upload = data.ulStatus
let ChartData = this.h5SpeedtestUploadSpeedChart.series[0].data
let categories = this.h5SpeedtestUploadSpeedChart.chartOptions.xaxis.categories
ChartData.push(this.h5Upload)
categories.push(nowPointName)
this.h5SpeedtestUploadSpeedChart.chartOptions.value = {
xaxis: { categories: categories }
}
}
}
}
}
this.h5SpeedWorker.postMessage('start ' + JSON.stringify({
test_order: "D_U",
url_dl: 'speedtest/download',
url_ul: 'speedtest/upload',
url_ping: 'speedtest/upload',
}));
this.h5SpeedWorkerTimer = setInterval(() => {
this.h5SpeedWorker.postMessage('status')
}, 200);
// this.h5SpeedtestWorking = !this.h5SpeedtestWorking
}
},
data() {
return {
h5SpeedWorker: null,
h5SpeedWorkerTimer: null,
h5SpeedtestWorking: false,
h5SpeedtestButtonText: '开始测速',
h5Upload: '...',
h5Download: '...',
h5SpeedtestDownloadSpeedChart: {
chartOptions: {
chart: {
id: "speedtest-download-chart",
height: 200,
foreColor: '#e8e8e8',
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
speed: 300
},
},
zoom: {
enabled: false
},
toolbar: {
show: false,
},
tooltip: {
theme: 'dark'
},
},
xaxis: {
type: 'category',
categories: [''],
labels: {
show: false
}
},
yaxis: {
labels: {
formatter: (value) => {
return value + ' Mbps'
}
}
},
dataLabels: {
enabled: false
},
markers: {
size: 0
},
stroke: {
curve: 'smooth'
},
},
series: [
{
type: 'line',
name: 'Receive',
data: []
}
]
},
h5SpeedtestUploadSpeedChart: {
chartOptions: {
chart: {
id: "speedtest-upload-chart",
height: 200,
foreColor: '#e8e8e8',
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
speed: 300
},
},
zoom: {
enabled: false
},
toolbar: {
show: false,
},
tooltip: {
theme: 'dark'
},
},
xaxis: {
type: 'category',
categories: [''],
labels: {
show: false
}
},
yaxis: {
labels: {
formatter: (value) => {
return value + ' Mbps'
}
}
},
dataLabels: {
enabled: false
},
markers: {
size: 0
},
stroke: {
curve: 'smooth'
},
},
series: [
{
type: 'line',
name: 'Receive',
data: []
}
]
},
}
},
mounted() {
// setInterval(() => {
// console.log(this.componentConfig)
// }, 1000)
}
})
</script>

View File

@@ -0,0 +1,207 @@
<template>
<div>
<n-card title="Server Traffic" hoverable>
<n-grid x-gap="12" cols="1 s:1 m:1 l:2 xl:2 2xl:2" responsive="screen">
<n-gi v-for="(interfaceData, interfaceName) in interfaces">
<n-card :title="interfaceName">
<n-grid x-gap="12" :cols="2">
<n-gi>
<h3>已接收</h3>
<span class="traffic-display">
{{ formatBytes(interfaceData.traffic.receive, 2, true) }} /
{{ formatBytes(interfaceData.receive) }}
</span>
</n-gi>
<n-gi>
<h3>已发送</h3>
<span class="traffic-display">
{{ formatBytes(interfaceData.traffic.send, 2, true) }} /
{{ formatBytes(interfaceData.send) }}
</span>
</n-gi>
<n-gi span="2">
<apexchart type="line" :options="interfaceData.chartOptions"
:series="interfaceData.series">
</apexchart>
</n-gi>
</n-grid>
</n-card>
</n-gi>
</n-grid>
</n-card>
</div>
</template>
<script>
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
components: {
apexchart: defineAsyncComponent(() => import('vue3-apexcharts')),
},
props: {
wsMessage: Array
},
data() {
return {
traffic: {
receive: null,
send: null
},
categories: [],
refreshTimer: null,
interfaces: {}
}
},
methods: {
updateSeries() {
var nowPointName = (new Date()).getHours().toString().padStart(2, '0') + ':' +
(new Date()).getMinutes().toString().padStart(2, '0') + ':' +
(new Date()).getSeconds().toString().padStart(2, '0')
for (let interfaceName in this.interfaces) {
let categories = this.interfaces[interfaceName].chartOptions.xaxis.categories
let receiveDatas = this.interfaces[interfaceName].series[0].data
let sendDatas = this.interfaces[interfaceName].series[1].data
let receive = this.interfaces[interfaceName].receive - this.interfaces[interfaceName].lastReceive
let send = this.interfaces[interfaceName].send - this.interfaces[interfaceName].lastSend
this.interfaces[interfaceName].lastReceive = this.interfaces[interfaceName].receive
this.interfaces[interfaceName].lastSend = this.interfaces[interfaceName].send
this.interfaces[interfaceName].traffic.receive = receive
this.interfaces[interfaceName].traffic.send = send
receiveDatas.push(receive)
sendDatas.push(send)
categories.push(nowPointName)
this.interfaces[interfaceName].chartOptions.value = {
xaxis: { categories: categories }
}
}
},
formatBytes(bytes, decimals = 2, bandwidth = false) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const bandwidthSizes = ['Bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbs', 'Ebps', 'Zbps', 'Ybps'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
if (bandwidth) {
bytes = bytes * 10
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + bandwidthSizes[i];
}
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
},
mounted() {
setInterval(() => {
this.updateSeries()
}, 1000)
this.$watch(() => this.wsMessage, () => {
this.wsMessage.forEach((e, i) => {
if (e[0] != 100) return false
let interfaceName = e[1]
let receiveTraffic = e[2]
let sendTraffic = e[3]
this.wsMessage.splice(i, 1)
if (!this.interfaces.hasOwnProperty(interfaceName)) {
this.interfaces[interfaceName] = {
traffic: {
receive: null,
send: null
},
receive: receiveTraffic,
send: sendTraffic,
lastReceive: receiveTraffic,
lastSend: sendTraffic,
chartOptions: {
chart: {
id: "interface-" + interfaceName + "-chart",
foreColor: '#e8e8e8',
animations: {
enabled: true,
easing: 'linear',
dynamicAnimation: {
speed: 1000
},
},
zoom: {
enabled: false
},
toolbar: {
show: false,
},
tooltip: {
theme: 'dark'
},
},
xaxis: {
range: 10,
type: 'category',
categories: [''],
},
yaxis: {
labels: {
formatter: (value) => {
return this.formatBytes(value, 2, true)
}
}
},
tooltip: {
x: {
format: 'dd MMM yyyy'
}
},
dataLabels: {
enabled: false
},
markers: {
size: 0
},
stroke: {
curve: 'smooth'
},
},
series: [
{
type: 'line',
name: 'Receive',
data: []
},
{
type: 'line',
name: 'Send',
data: []
}
]
}
return;
}
this.interfaces[interfaceName].receive = receiveTraffic
this.interfaces[interfaceName].send = sendTraffic
})
}, { immediate: true, deep: true });
}
})
</script>
<style scoped>
h3 {
text-align: center;
}
.traffic-display {
text-align: center;
display: block;
}
</style>
<style>
.apexcharts-tooltip-title,
.apexcharts-tooltip-text {
color: #181818;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<n-card title="Network Utilities">
<n-space>
<n-button @click="activate('ping')">Ping</n-button>
<n-button @click="activate('traceroute')">Traceroute</n-button>
<n-button @click="activate('iperf3')">iPerf3</n-button>
</n-space>
</n-card>
<n-drawer v-model:show="componentSwitch.ping" :native-scrollbar="true" :width="drawWidth" placement="right">
<n-drawer-content title="Ping" :closable="true">
<ping v-model:ws="ws" v-model:wsMessage="wsMessage" />
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="componentSwitch.traceroute" :native-scrollbar="true" :width="drawWidth" placement="right">
<n-drawer-content title="Traceroute" :closable="true">
<traceroute v-model:ws="ws" v-model:wsMessage="wsMessage" />
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="componentSwitch.iperf3" :native-scrollbar="true" :width="drawWidth" placement="right">
<n-drawer-content title="iPerf3" :closable="true">
<iperf3 v-model:ws="ws" v-model:wsMessage="wsMessage" v-model:componentConfig="componentConfig" />
</n-drawer-content>
</n-drawer>
</template>
<script>
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
components: {
ping: defineAsyncComponent(() => import('./Utilities/Ping.vue')),
traceroute: defineAsyncComponent(() => import('./Utilities/Traceroute.vue')),
iperf3: defineAsyncComponent(() => import('./Utilities/iPerf3.vue')),
},
props: {
wsMessage: Array,
ws: WebSocket,
componentConfig: Object
},
data() {
return {
drawWidth: 800,
componentSwitch: {
ping: false,
traceroute: false,
iperf3: false
}
}
},
mounted() {
if (window.screen.width < 800) {
this.drawWidth = window.screen.width
}
},
methods: {
activate(args) {
this.host = ''
this.componentSwitch[args] = true
}
}
})
</script>

View File

@@ -0,0 +1,92 @@
<template>
<n-space vertical>
<n-input-group>
<n-input :disabled="working" v-model:value="host" placeholder="IP Address Or Domain" @keyup.enter="ping" />
<n-button :loading="working" type="primary" ghost @click="ping()">
Ping
</n-button>
</n-input-group>
<n-table v-show="records.length > 0" :bordered="false" :single-line="false">
<thead>
<tr>
<th>#</th>
<th>Host</th>
<th>TTL</th>
<th>Latency</th>
</tr>
</thead>
<tbody>
<tr v-for="record in records">
<td>{{ record.seq }}</td>
<td>{{ record.host }}</td>
<td>{{ record.ttl }}</td>
<td>{{ record.latency }} ms</td>
</tr>
</tbody>
</n-table>
</n-space>
</template>
<script>
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
props: {
wsMessage: Array,
ws: WebSocket
},
data() {
return {
host: '',
working: false,
records: []
}
},
methods: {
ping() {
if (this.working) return false;
this.records = []
this.working = true
this.ws.send('1|' + this.host)
let ticket = ''
let pingProcess = this.$watch(() => this.wsMessage, (e) => {
this.wsMessage.forEach((e, i) => {
if (e[0] != 1) return true
if (ticket.length == 0 && e[2] == this.host && e.length == 4) {
ticket = e[3]
this.wsMessage.splice(i, 1)
return true;
}
if (ticket == e[1] && e[2] == '0') {
this.working = false
pingProcess()
this.wsMessage.splice(i, 1)
return false;
}
if (ticket == e[1] && e[2] == '1') {
if (e.length == 7) {
this.records.push({
host: e[3],
seq: e[4],
ttl: e[5],
latency: e[6]
})
} else {
this.records.push({
host: '-',
seq: this.records.length + 1,
ttl: '-',
latency: '-'
})
}
this.wsMessage.splice(i, 1)
return true;
}
})
}, { immediate: true, deep: true })
}
}
})
</script>

View File

@@ -0,0 +1,138 @@
<template>
<n-space vertical>
<n-input-group>
<n-input :disabled="working" v-model:value="host" :style="{ width: '90%' }"
placeholder="IP Address Or Domain" @keyup.enter="traceroute" />
<n-button :loading="working" type="primary" ghost @click="traceroute()">
Traceroute
</n-button>
</n-input-group>
<n-table v-show="records.length > 0" :bordered="false" :single-line="false">
<thead>
<tr>
<th>Hop #</th>
<th>Host</th>
<th>#1</th>
<th>#2</th>
<th>#3</th>
</tr>
</thead>
<tbody>
<template v-for="(record, seq) in records">
<tr v-if="record">
<td>{{ seq }}</td>
<td>
<n-space vertical>
<n-gradient-text v-show="record.host.length > 1" type="info">
! 基于流的负载均衡已发现
</n-gradient-text>
<template v-for="pop in record.host">
<span>{{ pop.dns }} ({{ pop.host }}) | {{ pop.geo }}</span>
</template>
</n-space>
</td>
<td>
<template v-if="record.latency[0]">
{{ record.latency[0] }} ms
</template>
<template v-else>
-
</template>
</td>
<td> <template v-if="record.latency[1]">
{{ record.latency[1] }} ms
</template>
<template v-else>
-
</template>
</td>
<td> <template v-if="record.latency[2]">
{{ record.latency[2] }} ms
</template>
<template v-else>
-
</template>
</td>
</tr>
</template>
</tbody>
</n-table>
</n-space>
</template>
<script>
import { method } from 'lodash'
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
props: {
wsMessage: Array,
ws: WebSocket
},
data() {
return {
host: '',
working: false,
records: []
}
},
methods: {
traceroute() {
if (this.working) return false;
this.records = []
this.working = true
this.ws.send('2|' + this.host)
let ticket = ''
let traceProcess = this.$watch(() => this.wsMessage, (e) => {
this.wsMessage.forEach((e, i) => {
if (e[0] != 2) return true
if (ticket.length == 0 && e[2] == this.host && e.length == 4) {
ticket = e[3]
this.wsMessage.splice(i, 1)
return true;
}
if (ticket == e[1] && e[2] == '0') {
this.working = false
traceProcess()
this.wsMessage.splice(i, 1)
return false;
}
if (ticket == e[1] && e[2] == '1') {
if (this.records[e[3]] === undefined) {
this.records[e[3]] = {
host: [{
dns: e[4] == '0' ? '-' : e[4],
host: e[5] == '0' ? '-' : e[5],
geo: e[7],
}],
latency: [e[6]]
}
} else {
this.records[e[3]].host.push({
dns: e[4] == '0' ? '-' : e[4],
host: e[5] == '0' ? '-' : e[5],
geo: e[7],
})
this.records[e[3]].latency.push(e[6])
}
this.records[e[3]].host.forEach((v1, k1) => {
this.records[e[3]].host.forEach((v2, k2) => {
if (k1 != k2 && v1.host == v2.host) {
this.records[e[3]].host.splice(k2, 1)
}
})
if (v1.dns == '-' && this.records[e[3]].host.length > 1) {
this.records[e[3]].host.splice(k1, 1)
}
})
this.wsMessage.splice(i, 1)
return true;
}
})
}, { immediate: true, deep: true })
}
}
})
</script>

View File

@@ -0,0 +1,102 @@
<template>
<n-space vertical>
<n-button :block="true" :loading="working" type="primary" ghost @click="startServer()">
{{ btnText }}
</n-button>
<n-progress v-show="timeout != 0" style="transform: rotate(180deg)" type="line"
:percentage="100 - TimeoutPercentage" :show-indicator="false" />
<n-alert v-show="working && port != 0" type="default" :show-icon="false">
iPerf3 command:
<p v-show="componentConfig.public_ipv4">
IPv4:<br />
iperf3 -c {{ componentConfig.public_ipv4 }} -p {{ port }}
</p>
<p v-show="componentConfig.public_ipv6">
IPv6:<br />
iperf3 -c {{ componentConfig.public_ipv6 }} -p {{ port }}
</p>
</n-alert>
<n-input autosize style="font-family: monospace;" v-show="log.length > 0" row="30" type="textarea" :value="log"
placeholder="iPerf3 Server log" disabled />
</n-space>
</template>
<script>
import { defineComponent, defineAsyncComponent } from 'vue'
export default defineComponent({
props: {
wsMessage: Array,
ws: WebSocket,
componentConfig: Object
},
data() {
return {
btnText: 'Start iPerf3 Server',
working: false,
log: '',
port: 0,
timeout: 0,
timePass: 0,
TimeoutPercentage: 0,
timeoutTimer: null,
}
},
methods: {
startServer() {
if (this.working) return false;
this.btnText = 'iPerf3 Server starting...'
this.log = ''
this.working = true
this.ws.send('4')
let ticket = ''
let iperfProcess = this.$watch(() => this.wsMessage, (e) => {
this.wsMessage.forEach((e, i) => {
if (e[0] != 4) return true
if (ticket.length == 0 && e[1] == '1' && e.length == 3) {
ticket = e[2]
this.wsMessage.splice(i, 1)
return true;
}
if (ticket == e[1] && e[2] == '0') {
this.working = false
this.port = 0
this.timeout = 0
clearInterval(this.timeoutTimer)
this.timePass = 0
iperfProcess()
this.btnText = 'Start iPerf3 Server'
this.wsMessage.splice(i, 1)
return false;
}
if (ticket == e[1] && e[2] == '1') {
this.port = e[3]
this.timeout = e[4]
this.timeoutTimer = setInterval(() => {
this.btnText = 'iPerf3 Server started (' + (this.timeout - this.timePass) + 's left)'
this.timePass++
this.TimeoutPercentage = Math.floor((this.timePass / this.timeout) * 100)
}, 1000)
this.wsMessage.splice(i, 1)
return false;
}
if (ticket == e[1] && e[2] == '2') {
console.log(this.log)
if (e[3].length > 0) {
this.log = this.log + e[3]
}
this.wsMessage.splice(i, 1)
return false;
}
})
}, { immediate: true, deep: true })
}
}
})
</script>

4
ui/src/main.js Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

36
ui/vite.config.js Normal file
View File

@@ -0,0 +1,36 @@
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
Components({
resolvers: [NaiveUiResolver()]
})],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/ws': {
target: 'ws://127.0.0.1:80',
ws: true
},
'/speedtest-static': {
target: 'http://127.0.0.1:80',
},
'/speedtest/': {
target: 'http://127.0.0.1:80',
}
}
}
})