diff --git a/web/frps/.babelrc b/web/frps/.babelrc new file mode 100644 index 0000000..5eae2e8 --- /dev/null +++ b/web/frps/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + ["es2015", { "modules": false }] + ] +} \ No newline at end of file diff --git a/web/frps/.gitignore b/web/frps/.gitignore new file mode 100644 index 0000000..3cd34b4 --- /dev/null +++ b/web/frps/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log +.idea +.vscode/settings.json diff --git a/web/frps/Makefile b/web/frps/Makefile new file mode 100644 index 0000000..42dec29 --- /dev/null +++ b/web/frps/Makefile @@ -0,0 +1,9 @@ +.PHONY: dist build +install: + @npm install + +dev: install + @npm run dev + +build: + @npm run build \ No newline at end of file diff --git a/web/frps/package.json b/web/frps/package.json new file mode 100644 index 0000000..9d41c4c --- /dev/null +++ b/web/frps/package.json @@ -0,0 +1,46 @@ +{ + "name": "frps-dashboard", + "description": "A dashboard for frp server.", + "author": "fatedier", + "private": true, + "scripts": { + "dev": "webpack-dev-server -d --inline --hot --env.dev", + "build": "rimraf dist && webpack -p --progress --hide-modules" + }, + "dependencies": { + "bootstrap": "^3.3.7", + "echarts": "^3.5.0", + "element-ui": "^1.2.5", + "humanize-plus": "^1.8.2", + "vue": "^2.2.4", + "vue-resource": "^1.2.1", + "vue-router": "^2.3.0" + }, + "engines": { + "node": ">=6" + }, + "devDependencies": { + "autoprefixer": "^6.6.0", + "babel-core": "^6.21.0", + "babel-eslint": "^7.1.1", + "babel-loader": "^6.4.0", + "babel-preset-es2015": "^6.13.2", + "css-loader": "^0.27.0", + "eslint": "^3.12.2", + "eslint-config-enough": "^0.2.2", + "eslint-loader": "^1.6.3", + "file-loader": "^0.10.1", + "html-loader": "^0.4.5", + "html-webpack-plugin": "^2.24.1", + "less": "^2.7.2", + "less-loader": "^3.0.0", + "postcss-loader": "^1.3.3", + "rimraf": "^2.5.4", + "style-loader": "^0.13.2", + "url-loader": "^0.5.8", + "vue-loader": "^11.1.4", + "vue-template-compiler": "^2.1.8", + "webpack": "^2.2.0-rc.4", + "webpack-dev-server": "beta" + } +} diff --git a/web/frps/postcss.config.js b/web/frps/postcss.config.js new file mode 100644 index 0000000..af65640 --- /dev/null +++ b/web/frps/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer')() + ] +} \ No newline at end of file diff --git a/web/frps/src/App.vue b/web/frps/src/App.vue new file mode 100644 index 0000000..070e781 --- /dev/null +++ b/web/frps/src/App.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/web/frps/src/assets/favicon.ico b/web/frps/src/assets/favicon.ico new file mode 100644 index 0000000..4347765 Binary files /dev/null and b/web/frps/src/assets/favicon.ico differ diff --git a/web/frps/src/components/Overview.vue b/web/frps/src/components/Overview.vue new file mode 100644 index 0000000..1358523 --- /dev/null +++ b/web/frps/src/components/Overview.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/web/frps/src/components/ProxiesHttp.vue b/web/frps/src/components/ProxiesHttp.vue new file mode 100644 index 0000000..5aa7691 --- /dev/null +++ b/web/frps/src/components/ProxiesHttp.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/web/frps/src/components/ProxiesHttps.vue b/web/frps/src/components/ProxiesHttps.vue new file mode 100644 index 0000000..bd8bc55 --- /dev/null +++ b/web/frps/src/components/ProxiesHttps.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/web/frps/src/components/ProxiesTcp.vue b/web/frps/src/components/ProxiesTcp.vue new file mode 100644 index 0000000..bf4d220 --- /dev/null +++ b/web/frps/src/components/ProxiesTcp.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/web/frps/src/components/ProxiesUdp.vue b/web/frps/src/components/ProxiesUdp.vue new file mode 100644 index 0000000..2b46860 --- /dev/null +++ b/web/frps/src/components/ProxiesUdp.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/web/frps/src/components/Traffic.vue b/web/frps/src/components/Traffic.vue new file mode 100644 index 0000000..ae57364 --- /dev/null +++ b/web/frps/src/components/Traffic.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/web/frps/src/index.html b/web/frps/src/index.html new file mode 100644 index 0000000..aebd655 --- /dev/null +++ b/web/frps/src/index.html @@ -0,0 +1,15 @@ + + + + + + frps dashboard + + + +
+ + + + + diff --git a/web/frps/src/main.js b/web/frps/src/main.js new file mode 100644 index 0000000..d41949e --- /dev/null +++ b/web/frps/src/main.js @@ -0,0 +1,17 @@ +import Vue from 'vue' +import ElementUI from 'element-ui' +import 'element-ui/lib/theme-default/index.css' +import './utils/less/custom.less' + +import App from './App.vue' +import router from './router' + +Vue.use(ElementUI) +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + template: '', + components: { App } +}) diff --git a/web/frps/src/router/index.js b/web/frps/src/router/index.js new file mode 100644 index 0000000..bd77e97 --- /dev/null +++ b/web/frps/src/router/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Overview from '../components/Overview.vue' +import ProxiesTcp from '../components/ProxiesTcp.vue' +import ProxiesUdp from '../components/ProxiesUdp.vue' +import ProxiesHttp from '../components/ProxiesHttp.vue' +import ProxiesHttps from '../components/ProxiesHttps.vue' + +Vue.use(Router) + +export default new Router({ + routes: [{ + path: '/', + name: 'Overview', + component: Overview + }, { + path: '/proxies/tcp', + name: 'ProxiesTcp', + component: ProxiesTcp + }, { + path: '/proxies/udp', + name: 'ProxiesUdp', + component: ProxiesUdp + }, { + path: '/proxies/http', + name: 'ProxiesHttp', + component: ProxiesHttp + }, { + path: '/proxies/https', + name: 'ProxiesHttps', + component: ProxiesHttps + }] +}) \ No newline at end of file diff --git a/web/frps/src/utils/chart.js b/web/frps/src/utils/chart.js new file mode 100644 index 0000000..82d45f7 --- /dev/null +++ b/web/frps/src/utils/chart.js @@ -0,0 +1,187 @@ +import Humanize from "humanize-plus" +import echarts from "echarts/lib/echarts" + +import "echarts/theme/macarons" +import "echarts/lib/chart/bar" +import "echarts/lib/chart/pie" +import "echarts/lib/component/tooltip" +import "echarts/lib/component/title" + +function DrawTrafficChart(elementId, trafficIn, trafficOut) { + let myChart = echarts.init(document.getElementById(elementId), 'macarons'); + myChart.showLoading() + + let option = { + title: { + text: 'Network Traffic', + subtext: 'today', + x: 'center' + }, + tooltip: { + trigger: 'item', + formatter: function(v) { + return Humanize.fileSize(v.data.value) + " (" + v.percent + "%)" + } + }, + series: [{ + type: 'pie', + radius: '55%', + center: ['50%', '60%'], + data: [{ + value: trafficIn, + name: 'Traffic In' + }, { + value: trafficOut, + name: 'Traffic Out' + }, ], + itemStyle: { + emphasis: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }] + }; + myChart.setOption(option); + myChart.hideLoading() +} + +function DrawProxyChart(elementId, serverInfo) { + if (serverInfo.proxy_type_count.tcp == null) { + serverInfo.proxy_type_count.tcp = 0 + } + if (serverInfo.proxy_type_count.udp == null) { + serverInfo.proxy_type_count.udp = 0 + } + if (serverInfo.proxy_type_count.http == null) { + serverInfo.proxy_type_count.http = 0 + } + if (serverInfo.proxy_type_count.https == null) { + serverInfo.proxy_type_count.https = 0 + } + let myChart = echarts.init(document.getElementById(elementId), 'macarons') + myChart.showLoading() + + let option = { + title: { + text: 'Proxies', + subtext: 'now', + x: 'center' + }, + tooltip: { + trigger: 'item', + formatter: function(v) { + return v.data.value + } + }, + series: [{ + type: 'pie', + radius: '55%', + center: ['50%', '60%'], + data: [{ + value: serverInfo.proxy_type_count.tcp, + name: 'TCP' + }, { + value: serverInfo.proxy_type_count.udp, + name: 'UDP' + }, { + value: serverInfo.proxy_type_count.http, + name: 'HTTP' + }, { + value: serverInfo.proxy_type_count.https, + name: 'HTTPS' + }], + itemStyle: { + emphasis: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }] + }; + myChart.setOption(option); + myChart.hideLoading() +} + +// 7 days +function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) { + let params = { + width: '600px', + height: '400px' + } + + let myChart = echarts.init(document.getElementById(elementId), 'macarons', params); + myChart.showLoading() + + trafficInArr = trafficInArr.reverse() + trafficOutArr = trafficOutArr.reverse() + let now = new Date() + now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6) + let dates = new Array() + for (let i = 0; i < 7; i++) { + dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate()) + now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) + } + + let option = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: function(data) { + let html = '' + if (data.length > 0) { + html += data[0].name + '
' + } + for (let v of data) { + let colorEl = ''; + html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '
' + } + return html + } + }, + legend: { + data: ['Traffic In', 'Traffic Out'] + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: [{ + type: 'category', + data: dates + }], + yAxis: [{ + type: 'value', + axisLabel: { + formatter: function(value) { + return Humanize.fileSize(value) + } + } + }], + series: [{ + name: 'Traffic In', + type: 'bar', + data: trafficInArr + }, { + + name: 'Traffic Out', + type: 'bar', + data: trafficOutArr + }] + }; + myChart.setOption(option); + myChart.hideLoading() +} + +export { + DrawTrafficChart, + DrawProxyChart, + DrawProxyTrafficChart +} diff --git a/web/frps/src/utils/less/custom.less b/web/frps/src/utils/less/custom.less new file mode 100644 index 0000000..b83a400 --- /dev/null +++ b/web/frps/src/utils/less/custom.less @@ -0,0 +1,22 @@ +@color: red; + +.el-form-item { + span { + margin-left: 15px; + } +} + +.demo-table-expand { + font-size: 0; + + label { + width: 90px; + color: #99a9bf; + } + + .el-form-item { + margin-right: 0; + margin-bottom: 0; + width: 50%; + } +} diff --git a/web/frps/src/utils/proxy.js b/web/frps/src/utils/proxy.js new file mode 100644 index 0000000..5f2bbee --- /dev/null +++ b/web/frps/src/utils/proxy.js @@ -0,0 +1,88 @@ +class BaseProxy { + constructor(proxyStats) { + this.name = proxyStats.name + if (proxyStats.conf != null) { + this.encryption = proxyStats.conf.use_encryption + this.compression = proxyStats.conf.use_compression + } else { + this.encryption = "" + this.compression = "" + } + this.conns = proxyStats.cur_conns + this.traffic_in = proxyStats.today_traffic_in + this.traffic_out = proxyStats.today_traffic_out + this.status = proxyStats.status + } +} + +class TcpProxy extends BaseProxy { + constructor(proxyStats) { + super(proxyStats) + this.type = "tcp" + if (proxyStats.conf != null) { + this.addr = proxyStats.conf.bind_addr + ":" + proxyStats.conf.remote_port + this.port = proxyStats.conf.remote_port + } else { + this.addr = "" + this.port = "" + } + } +} + +class UdpProxy extends BaseProxy { + constructor(proxyStats) { + super(proxyStats) + this.type = "udp" + if (proxyStats.conf != null) { + this.addr = proxyStats.conf.bind_addr + ":" + proxyStats.conf.remote_port + this.port = proxyStats.conf.remote_port + } else { + this.addr = "" + this.port = "" + } + } +} + +class HttpProxy extends BaseProxy { + constructor(proxyStats, port, subdomain_host) { + super(proxyStats) + this.type = "http" + this.port = port + if (proxyStats.conf != null) { + this.custom_domains = proxyStats.conf.custom_domains + this.host_header_rewrite = proxyStats.conf.host_header_rewrite + this.locations = proxyStats.conf.locations + if (proxyStats.conf.sub_domain != "") { + this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host + } else { + this.subdomain = "" + } + } else { + this.custom_domains = "" + this.host_header_rewrite = "" + this.subdomain = "" + this.locations = "" + } + } +} + +class HttpsProxy extends BaseProxy { + constructor(proxyStats, port, subdomain_host) { + super(proxyStats) + this.type = "https" + this.port = port + if (proxyStats.conf != null) { + this.custom_domains = proxyStats.conf.custom_domains + if (proxyStats.conf.sub_domain != "") { + this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host + } else { + this.subdomain = "" + } + } else { + this.custom_domains = "" + this.subdomain = "" + } + } +} + +export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy} diff --git a/web/frps/src/vendor.js b/web/frps/src/vendor.js new file mode 100644 index 0000000..986e5d0 --- /dev/null +++ b/web/frps/src/vendor.js @@ -0,0 +1,2 @@ +import Vue from 'vue' +import ElementUI from 'element-ui' \ No newline at end of file diff --git a/web/frps/webpack.config.js b/web/frps/webpack.config.js new file mode 100644 index 0000000..74503ab --- /dev/null +++ b/web/frps/webpack.config.js @@ -0,0 +1,93 @@ +const path = require('path') +var webpack = require('webpack') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var url = require('url') +var publicPath = '' + +module.exports = (options = {}) => ({ + entry: { + vendor: './src/vendor', + index: './src/main.js' + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: options.dev ? '[name].js' : '[name].js?[chunkhash]', + chunkFilename: '[id].js?[chunkhash]', + publicPath: options.dev ? '/assets/' : publicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@': path.resolve(__dirname, 'src'), + } + }, + module: { + rules: [{ + test: /\.vue$/, + use: ['vue-loader'] + }, { + test: /\.js$/, + use: ['babel-loader'], + exclude: /node_modules/ + }, { + test: /\.html$/, + use: [{ + loader: 'html-loader', + options: { + root: path.resolve(__dirname, 'src'), + attrs: ['img:src', 'link:href'] + } + }] + }, { + test: /\.less$/, + loader: 'style-loader!css-loader!postcss-loader!less-loader' + }, { + test: /\.css$/, + use: ['style-loader', 'css-loader', 'postcss-loader'] + }, { + test: /favicon\.png$/, + use: [{ + loader: 'file-loader', + options: { + name: '[name].[ext]?[hash]' + } + }] + }, { + test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, + exclude: /favicon\.png$/, + use: [{ + loader: 'url-loader', + options: { + limit: 10000 + } + }] + }] + }, + plugins: [ + new webpack.optimize.CommonsChunkPlugin({ + names: ['vendor', 'manifest'] + }), + new HtmlWebpackPlugin({ + favicon: 'src/assets/favicon.ico', + template: 'src/index.html' + }) + ], + devServer: { + host: '127.0.0.1', + port: 8010, + proxy: { + '/api/': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + pathRewrite: { + '^/api': '' + } + } + }, + historyApiFallback: { + index: url.parse(options.dev ? '/assets/' : publicPath).pathname + } + }//, + //devtool: options.dev ? '#eval-source-map' : '#source-map' +})