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 @@
+
+
+
+
+
+
+
+ Overview
+
+ Proxies
+ TCP
+ UDP
+ HTTP
+ HTTPS
+
+ Help
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ {{ vhost_http_port }}
+
+
+ {{ vhost_https_port }}
+
+
+ {{ auth_timeout }}
+
+
+ {{ subdomain_host }}
+
+
+ {{ max_pool_count }}
+
+
+ {{ heart_beat_timeout }}
+
+
+ {{ client_counts }}
+
+
+ {{ cur_conns }}
+
+
+ {{ proxy_counts }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ Traffic Statistics
+
+
+
+ {{ props.row.name }}
+
+
+ {{ props.row.type }}
+
+
+ {{ props.row.custom_domains }}
+
+
+ {{ props.row.subdomain }}
+
+
+ {{ props.row.locations }}
+
+
+ {{ props.row.host_header_rewrite }}
+
+
+ {{ props.row.encryption }}
+
+
+ {{ props.row.compression }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status }}
+ {{ scope.row.status }}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ Traffic Statistics
+
+
+
+ {{ props.row.name }}
+
+
+ {{ props.row.type }}
+
+
+ {{ props.row.custom_domains }}
+
+
+ {{ props.row.subdomain }}
+
+
+ {{ props.row.encryption }}
+
+
+ {{ props.row.compression }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status }}
+ {{ scope.row.status }}
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ Traffic Statistics
+
+
+
+ {{ props.row.name }}
+
+
+ {{ props.row.type }}
+
+
+ {{ props.row.addr }}
+
+
+ {{ props.row.encryption }}
+
+
+ {{ props.row.compression }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status }}
+ {{ scope.row.status }}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ Traffic Statistics
+
+
+
+ {{ props.row.name }}
+
+
+ {{ props.row.type }}
+
+
+ {{ props.row.addr }}
+
+
+ {{ props.row.encryption }}
+
+
+ {{ props.row.compression }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.status }}
+ {{ scope.row.status }}
+
+
+
+
+
+
+
+
+
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'
+})