Compare commits

...

30 Commits

Author SHA1 Message Date
fatedier
b2ae433e18 Merge pull request #2206 from fatedier/dev
bump version
2021-01-19 20:56:06 +08:00
fatedier
b26080589b bump version 2021-01-19 20:51:06 +08:00
yuyulei
aff979c2b6 Merge pull request #2199 from fatedier/fix
vhost: set DisableKeepAlives = false and fix websocket not work
2021-01-19 15:40:41 +08:00
fatedier
46f809d711 vhost: set DisableKeepAlives = false and fix websocket not work 2021-01-18 21:49:44 +08:00
yuyulei
72595b2da8 Add user remote address info log (#2184) 2021-01-11 16:52:17 +08:00
XNG
c842558ace Reduced log level of "get hostname error" (#2165)
Reduced log level of "get hostname from http/https request error" to debug so that it won't overwhelms the log file when user is using apache/ngix as their own SSL backend
2020-12-25 13:39:26 +08:00
yuyulei
ed61049041 Bugfix: add ipv6 parsing with address of frps (#2163) 2020-12-24 21:48:26 +08:00
fatedier
abe6f580c0 update README 2020-12-03 20:41:37 +08:00
fatedier
e940066012 auto generate web assets 2020-12-03 20:23:43 +08:00
hubery
1e846df870 some dashboard refactor (#2101) 2020-12-03 20:20:48 +08:00
Mike Cardwell
0ab055e946 Allow server plugin to talk to https services. Option for skipping tls verification (#2103)
* Allow server plugin to talk to https services. Option for skipping tls verification

* Rename TlsVerify to TLSVerify

* Server plugin should use default http transport when scheme is not https
2020-12-03 14:36:14 +08:00
fatedier
fca59c71e2 clear Release.md 2020-11-23 17:34:09 +08:00
wxiaoguang
fae2f8768d tweak logs: (#2100)
* show what config frps uses (config file / command line)
* optimize the "start successfully" log message
2020-11-23 17:01:31 +08:00
fatedier
3d9499f554 autogen web assets 2020-11-23 15:21:07 +08:00
hubery
7adeeedd55 fix dashboard horizontal scrollbar (#2096) 2020-11-23 14:52:55 +08:00
yuyulei
127a31ea6a Fix typo (#2089) 2020-11-23 11:38:21 +08:00
fatedier
a85bd9a4d9 update 2020-11-20 20:10:08 +08:00
fatedier
01d551ec8d update 2020-11-20 18:10:33 +08:00
fatedier
16cabf4127 add .circleci 2020-11-20 18:04:44 +08:00
fatedier
968be4a2c2 use self token 2020-11-20 17:35:06 +08:00
fatedier
aa0a41ee4e Merge pull request #2088 from fatedier/dev
bump version to v0.34.3
2020-11-20 17:04:55 +08:00
fatedier
8a779eb88c add goreleaser action (#2087)
* add goreleaser action

* change version
2020-11-20 17:00:14 +08:00
蓝云Reyes
0138dbd352 update ISSUE_TEMPLATE (#2061) 2020-11-09 19:39:56 +08:00
yuyulei
9b45c93c14 Fix set-env in github actions of frp (#2060) 2020-11-09 16:56:53 +08:00
fatedier
191da54980 .travis.yml add go1.15 and remove go1.13 2020-11-06 15:49:03 +08:00
Dan Ordille
c3b7575453 Add enable_prometheus option as command line flag (#2057) 2020-11-06 15:33:59 +08:00
fatedier
1ea1530b36 Merge pull request #2058 from fatedier/dev
bump version to v0.34.2
2020-11-06 14:50:50 +08:00
fatedier
7f7305fa03 update version 2020-11-06 14:39:04 +08:00
yuyulei
644a0cfdb6 Update ReverseProxy code from official golang repo (#2051)
Fix #1192
2020-11-05 16:34:17 +08:00
Chirag Sukhala
3c2e2bcea5 Update frpc_full.ini (#2023)
Added authenticate_heartbeats and authenticate_new_work_conns
2020-10-08 14:25:03 +08:00
81 changed files with 11288 additions and 10721 deletions

25
.circleci/config.yml Normal file
View File

@@ -0,0 +1,25 @@
version: 2
jobs:
test1:
docker:
- image: circleci/golang:1.15-node
working_directory: /go/src/github.com/fatedier/frp
steps:
- checkout
- run: make
- run: make alltest
test2:
docker:
- image: circleci/golang:1.14-node
working_directory: /go/src/github.com/fatedier/frp
steps:
- checkout
- run: make
- run: make alltest
workflows:
version: 2
build_and_test:
jobs:
- test1
- test2

View File

@@ -1,29 +0,0 @@
Issue is only used for submiting bug report and documents typo. If there are same issues or answers can be found in documents, we will close it directly.
Use the commands below to provide key information from your environment:
You do NOT have to include this information if this is a FEATURE REQUEST
**What version of frp are you using (./frpc -v or ./frps -v)?**
**What operating system and processor architecture are you using (`go env`)?**
**Configures you used:**
**Steps to reproduce the issue:**
1.
2.
3.
**Describe the results you received:**
**Describe the results you expected:**
**Additional information you deem important (e.g. issue happens only occasionally):**
**Can you point out what caused this issue (optional)**

44
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,44 @@
---
name: Bug Report
about: Bug Report for FRP
title: ''
labels: Requires Testing
assignees: ''
---
<!-- From Chinese to English by machine translation, welcome to revise and polish. -->
<!-- ⚠️⚠️ Incomplete reports will be marked as invalid, and closed, with few exceptions ⚠️⚠️ -->
<!-- in addition, please use search well so that the same solution can be found in the feedback, we will close it directly -->
<!-- for convenience of differentiation, use FRPS or FRPC to refer to the FRP server or client -->
**[REQUIRED] hat version of frp are you using**
<!-- Use ./frpc -v or ./frps -v -->
Version:
**[REQUIRED] What operating system and processor architecture are you using**
OS:
CPU architecture:
**[REQUIRED] description of errors**
**confile**
<!-- Please pay attention to hiding the token, server_addr and other privacy information -->
**log file**
<!-- If the file is too large, use Pastebin, for example https://pastebin.ubuntu.com/ -->
**Steps to reproduce the issue**
1.
2.
3.
**Supplementary information**
**Can you guess what caused this issue**
**Checklist**:
<!--- Make sure you've completed the following steps (put an "X" between of brackets): -->
- [] I included all information required in the sections above
- [] I made sure there are no duplicates of this report [(Use Search)](https://github.com/fatedier/frp/issues?q=is%3Aissue)

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: DOCS
url: https://github.com/fatedier/frp
about: Here you can find out how to configure frp.

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: "[+] Enhancement"
assignees: ''
---
<!-- From Chinese to English by machine translation, welcome to revise and polish. -->
**The solution you want**
<!--A clear and concise description of the solution you want. -->
**Alternatives considered**
<!--A clear and concise description of any alternative solutions or features you have considered. -->
**How to implement this function**
<!--Implementation steps for the solution you want. -->
**Application scenarios of this function**
<!--Make a clear and concise description of the application scenario of the solution you want. -->

View File

@@ -75,41 +75,41 @@ jobs:
name: Get Image Tag Name name: Get Image Tag Name
run: | run: |
if [ x${{ github.event.inputs.tag }} == x"" ]; then if [ x${{ github.event.inputs.tag }} == x"" ]; then
echo ::set-env name=TAG_NAME::${GITHUB_REF#refs/*/} echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
else else
echo ::set-env name=TAG_NAME::${{ github.event.inputs.tag }} echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi fi
# prepare image tags # prepare image tags
- -
name: Prepare Image Tags name: Prepare Image Tags
run: | run: |
echo ::set-env name=DOCKERFILE_FRPC_PATH::dockerfiles/Dockerfile-for-frpc echo "DOCKERFILE_FRPC_PATH=dockerfiles/Dockerfile-for-frpc" >> $GITHUB_ENV
echo ::set-env name=DOCKERFILE_FRPS_PATH::dockerfiles/Dockerfile-for-frps echo "DOCKERFILE_FRPS_PATH=dockerfiles/Dockerfile-for-frps" >> $GITHUB_ENV
echo ::set-env name=TAG_FRPC::fatedier/frpc:$TAG_NAME echo "TAG_FRPC=fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
echo ::set-env name=TAG_FRPS::fatedier/frps:$TAG_NAME echo "TAG_FRPS=fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
echo ::set-env name=TAG_FRPC_GPR::ghcr.io/fatedier/frpc:$TAG_NAME echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
echo ::set-env name=TAG_FRPS_GPR::ghcr.io/fatedier/frps:$TAG_NAME echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
# build images # build images
- -
name: Build Images name: Build Images
run: | run: |
# for Docker hub # for Docker hub
docker build --file $DOCKERFILE_FRPC_PATH --tag $TAG_FRPC . docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC }} .
docker build --file $DOCKERFILE_FRPS_PATH --tag $TAG_FRPS . docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS }} .
# for GPR # for GPR
docker build --file $DOCKERFILE_FRPC_PATH --tag $TAG_FRPC_GPR . docker build --file ${{ env.DOCKERFILE_FRPC_PATH }} --tag ${{ env.TAG_FRPC_GPR }} .
docker build --file $DOCKERFILE_FRPS_PATH --tag $TAG_FRPS_GPR . docker build --file ${{ env.DOCKERFILE_FRPS_PATH }} --tag ${{ env.TAG_FRPS_GPR }} .
# push to dockerhub # push to dockerhub
- -
name: Publish to Dockerhub name: Publish to Dockerhub
run: | run: |
echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
docker push $TAG_FRPC docker push ${{ env.TAG_FRPC }}
docker push $TAG_FRPS docker push ${{ env.TAG_FRPS }}
# push to gpr # push to gpr
- -
name: Publish to GPR name: Publish to GPR
run: | run: |
echo ${{ secrets.GPR_TOKEN }} | docker login ghcr.io --username ${{ github.repository_owner }} --password-stdin echo ${{ secrets.GPR_TOKEN }} | docker login ghcr.io --username ${{ github.repository_owner }} --password-stdin
docker push $TAG_FRPC_GPR docker push ${{ env.TAG_FRPC_GPR }}
docker push $TAG_FRPS_GPR docker push ${{ env.TAG_FRPS_GPR }}

30
.github/workflows/goreleaser.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: goreleaser
on:
workflow_dispatch:
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
- name: Make All
run: |
./package.sh
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist --release-notes=./Release.md
env:
GITHUB_TOKEN: ${{ secrets.GPR_TOKEN }}

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ packages/
release/ release/
test/bin/ test/bin/
vendor/ vendor/
dist/
# Cache # Cache
*.swp *.swp

19
.goreleaser.yml Normal file
View File

@@ -0,0 +1,19 @@
builds:
- skip: true
checksum:
name_template: 'checksums.txt'
release:
# Same as for github
# Note: it can only be one: either github, gitlab or gitea
github:
owner: fatedier
name: frp
draft: false
# You can add extra pre-existing files to the release.
# The filename on the release will be the last part of the path (base). If
# another file with the same name exists, the latest one found will be used.
# Defaults to empty.
extra_files:
- glob: ./release/packages/*

View File

@@ -1,12 +0,0 @@
sudo: false
language: go
go:
- 1.13.x
- 1.14.x
install:
- make
script:
- make alltest

View File

@@ -20,10 +20,10 @@ fmt:
go fmt ./... go fmt ./...
frps: frps:
env CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frps ./cmd/frps
frpc: frpc:
env CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/frpc ./cmd/frpc
test: gotest test: gotest

View File

@@ -7,32 +7,29 @@ all: build
build: app build: app
app: app:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_darwin_amd64 ./cmd/frpc env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_darwin_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_darwin_amd64 ./cmd/frps env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_darwin_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_386 ./cmd/frpc env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_386 ./cmd/frpc
env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_386 ./cmd/frps env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_386 ./cmd/frps
env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_amd64 ./cmd/frpc env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_freebsd_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_amd64 ./cmd/frps env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_freebsd_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_386 ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_386 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_386 ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_386 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_amd64 ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_amd64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_amd64 ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_amd64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm64 ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_arm64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm64 ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_arm64 ./cmd/frps
env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_386.exe ./cmd/frpc env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_386.exe ./cmd/frpc
env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps_windows_386.exe ./cmd/frps env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_windows_386.exe ./cmd/frps
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_amd64.exe ./cmd/frpc env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_windows_amd64.exe ./cmd/frpc
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_windows_amd64.exe ./cmd/frps env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_windows_amd64.exe ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64 ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64 ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64 ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64 ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64le ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips64le ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64le ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips64le ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mips ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mips ./cmd/frps
env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mipsle ./cmd/frpc env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frpc_linux_mipsle ./cmd/frpc
env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mipsle ./cmd/frps env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_linux_mipsle ./cmd/frps
temp:
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./frps_linux_amd64 ./cmd/frps

View File

@@ -1,6 +1,6 @@
# frp # frp
[![Build Status](https://travis-ci.org/fatedier/frp.svg?branch=master)](https://travis-ci.org/fatedier/frp) [![Build Status](https://circleci.com/gh/fatedier/frp.svg?style=shield)](https://circleci.com/gh/fatedier/frp)
[![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases) [![GitHub release](https://img.shields.io/github/tag/fatedier/frp.svg?label=release)](https://github.com/fatedier/frp/releases)
[README](README.md) | [中文文档](README_zh.md) [README](README.md) | [中文文档](README_zh.md)

8
Release.md Normal file
View File

@@ -0,0 +1,8 @@
### New
* Server Plugin supports HTTPS.
### Fix
* Fix IPv6 address parse problem.
* HTTP type proxy can't handle websocket protocol due to error `Connection` header value.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.el-form-item span{margin-left:15px}.demo-table-expand{font-size:0}.demo-table-expand label{width:90px;color:#99a9bf}.demo-table-expand .el-form-item{margin-right:0;margin-bottom:0;width:50%}body{background-color:#fafafa;margin:0;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif}header{width:100%;height:60px}.header-color{background:#58b7ff}#content{margin-top:20px;padding-right:40px}.brand{color:#fff;background-color:transparent;margin-left:20px;float:left;line-height:25px;font-size:25px;padding:15px 15px;height:30px;text-decoration:none}.source{border:1px solid #eaeefb;border-radius:4px;transition:.2s;padding:24px}.server_info{margin-left:40px;font-size:0}.server_info label{width:150px;color:#99a9bf}.server_info .el-form-item{margin-right:0;margin-bottom:0;width:100%}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?14bea8276eef86cc7c61"></script><script type="text/javascript" src="vendor.js?51925ec1a77936b64d61"></script></body> </html> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>frps-dashboard</title><link href="css/app.808290ae.css" rel="preload" as="style"><link href="css/chunk-vendors.84bb20f7.css" rel="preload" as="style"><link href="js/app.bb942a48.js" rel="preload" as="script"><link href="js/chunk-vendors.4421b07d.js" rel="preload" as="script"><link href="css/chunk-vendors.84bb20f7.css" rel="stylesheet"><link href="css/app.808290ae.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but frps-dashboard doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.4421b07d.js"></script><script src="js/app.bb942a48.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(r&&r(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=n(n.s=u[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"51925ec1a77936b64d61"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,u.appendChild(i),c},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -17,10 +17,10 @@ package client
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt"
"io" "io"
"net" "net"
"runtime/debug" "runtime/debug"
"strconv"
"sync" "sync"
"time" "time"
@@ -222,8 +222,10 @@ func (ctl *Control) connectServer() (conn net.Conn, err error) {
return return
} }
} }
conn, err = frpNet.ConnectServerByProxyWithTLS(ctl.clientCfg.HTTPProxy, ctl.clientCfg.Protocol,
fmt.Sprintf("%s:%d", ctl.clientCfg.ServerAddr, ctl.clientCfg.ServerPort), tlsConfig) address := net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort))
conn, err = frpNet.ConnectServerByProxyWithTLS(ctl.clientCfg.HTTPProxy, ctl.clientCfg.Protocol, address, tlsConfig)
if err != nil { if err != nil {
xl.Warn("start new connection to server error: %v", err) xl.Warn("start new connection to server error: %v", err)
return return
@@ -295,7 +297,7 @@ func (ctl *Control) msgHandler() {
}() }()
defer ctl.msgHandlerShutdown.Done() defer ctl.msgHandlerShutdown.Done()
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartBeatInterval) * time.Second) hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
defer hbSend.Stop() defer hbSend.Stop()
hbCheck := time.NewTicker(time.Second) hbCheck := time.NewTicker(time.Second)
defer hbCheck.Stop() defer hbCheck.Stop()
@@ -314,7 +316,7 @@ func (ctl *Control) msgHandler() {
} }
ctl.sendCh <- pingMsg ctl.sendCh <- pingMsg
case <-hbCheck.C: case <-hbCheck.C:
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartBeatTimeout)*time.Second { if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
// let reader() stop // let reader() stop
ctl.conn.Close() ctl.conn.Close()

View File

@@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"runtime" "runtime"
"strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -215,8 +216,9 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
return return
} }
} }
conn, err = frpNet.ConnectServerByProxyWithTLS(svr.cfg.HTTPProxy, svr.cfg.Protocol,
fmt.Sprintf("%s:%d", svr.cfg.ServerAddr, svr.cfg.ServerPort), tlsConfig) address := net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort))
conn, err = frpNet.ConnectServerByProxyWithTLS(svr.cfg.HTTPProxy, svr.cfg.Protocol, address, tlsConfig)
if err != nil { if err != nil {
return return
} }

View File

@@ -157,17 +157,16 @@ func parseClientCommonCfgFromIni(content string) (config.ClientCommonConf, error
func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
cfg = config.GetDefaultClientConf() cfg = config.GetDefaultClientConf()
strs := strings.Split(serverAddr, ":") ipStr, portStr, err := net.SplitHostPort(serverAddr)
if len(strs) < 2 { if err != nil {
err = fmt.Errorf("invalid server_addr") err = fmt.Errorf("invalid server_addr: %v", err)
return return
} }
if strs[0] != "" {
cfg.ServerAddr = strs[0] cfg.ServerAddr = ipStr
} cfg.ServerPort, err = strconv.Atoi(portStr)
cfg.ServerPort, err = strconv.Atoi(strs[1])
if err != nil { if err != nil {
err = fmt.Errorf("invalid server_addr") err = fmt.Errorf("invalid server_addr: %v", err)
return return
} }

View File

@@ -49,6 +49,7 @@ var (
dashboardPort int dashboardPort int
dashboardUser string dashboardUser string
dashboardPwd string dashboardPwd string
enablePrometheus bool
assetsDir string assetsDir string
logFile string logFile string
logLevel string logLevel string
@@ -79,6 +80,7 @@ func init() {
rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port") rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port")
rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user") rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user")
rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password") rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password")
rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard")
rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file") rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days") rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days")
@@ -103,6 +105,7 @@ var rootCmd = &cobra.Command{
var cfg config.ServerCommonConf var cfg config.ServerCommonConf
var err error var err error
if cfgFile != "" { if cfgFile != "" {
log.Info("frps uses config file: %s", cfgFile)
var content string var content string
content, err = config.GetRenderedConfFromFile(cfgFile) content, err = config.GetRenderedConfFromFile(cfgFile)
if err != nil { if err != nil {
@@ -110,6 +113,7 @@ var rootCmd = &cobra.Command{
} }
cfg, err = parseServerCommonCfg(CfgFileTypeIni, content) cfg, err = parseServerCommonCfg(CfgFileTypeIni, content)
} else { } else {
log.Info("frps uses command line arguments for config")
cfg, err = parseServerCommonCfg(CfgFileTypeCmd, "") cfg, err = parseServerCommonCfg(CfgFileTypeCmd, "")
} }
if err != nil { if err != nil {
@@ -171,6 +175,7 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) {
cfg.DashboardPort = dashboardPort cfg.DashboardPort = dashboardPort
cfg.DashboardUser = dashboardUser cfg.DashboardUser = dashboardUser
cfg.DashboardPwd = dashboardPwd cfg.DashboardPwd = dashboardPwd
cfg.EnablePrometheus = enablePrometheus
cfg.LogFile = logFile cfg.LogFile = logFile
cfg.LogLevel = logLevel cfg.LogLevel = logLevel
cfg.LogMaxDays = logMaxDays cfg.LogMaxDays = logMaxDays
@@ -209,7 +214,7 @@ func runServer(cfg config.ServerCommonConf) (err error) {
if err != nil { if err != nil {
return err return err
} }
log.Info("start frps success") log.Info("frps started successfully")
svr.Run() svr.Run()
return return
} }

View File

@@ -22,9 +22,31 @@ log_max_days = 3
# disable log colors when log_file is console, default is false # disable log colors when log_file is console, default is false
disable_log_color = false disable_log_color = false
# for authentication # for authentication, should be same as your frps.ini
# authenticate_heartbeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false.
authenticate_heartbeats = false
# authenticate_new_work_conns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false.
authenticate_new_work_conns = false
# auth token
token = 12345678 token = 12345678
# oidc_client_id specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc".
# By default, this value is "".
oidc_client_id =
# oidc_client_secret specifies the client secret to use to get a token in OIDC authentication if AuthenticationMethod == "oidc".
# By default, this value is "".
oidc_client_secret =
# oidc_audience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "".
oidc_audience =
# oidc_token_endpoint_url specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "".
oidc_token_endpoint_url =
# set admin address for control frpc's action by http api such as reload # set admin address for control frpc's action by http api such as reload
admin_addr = 127.0.0.1 admin_addr = 127.0.0.1
admin_port = 7400 admin_port = 7400

View File

@@ -23,7 +23,7 @@ vhost_https_port = 443
# response header timeout(seconds) for vhost http server, default is 60s # response header timeout(seconds) for vhost http server, default is 60s
# vhost_http_timeout = 60 # vhost_http_timeout = 60
# TcpMuxHttpConnectPort specifies the port that the server listens for TCP # tcpmux_httpconnect_port specifies the port that the server listens for TCP
# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP # HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP
# requests on one single port. If it's not - it will listen on this value for # requests on one single port. If it's not - it will listen on this value for
# HTTP CONNECT requests. By default, this value is 0. # HTTP CONNECT requests. By default, this value is 0.
@@ -44,6 +44,7 @@ enable_prometheus = true
# dashboard assets directory(only for debug mode) # dashboard assets directory(only for debug mode)
# assets_dir = ./static # assets_dir = ./static
# console or real logFile path like ./frps.log # console or real logFile path like ./frps.log
log_file = ./frps.log log_file = ./frps.log
@@ -58,12 +59,12 @@ disable_log_color = false
# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true. # DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
detailed_errors_to_client = true detailed_errors_to_client = true
# AuthenticationMethod specifies what authentication method to use authenticate frpc with frps. # authentication_method specifies what authentication method to use authenticate frpc with frps.
# If "token" is specified - token will be read into login message. # If "token" is specified - token will be read into login message.
# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token". # If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token".
authentication_method = token authentication_method = token
# AuthenticateHeartBeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false. # authenticate_heartbeats specifies whether to include authentication token in heartbeats sent to frps. By default, this value is false.
authenticate_heartbeats = false authenticate_heartbeats = false
# AuthenticateNewWorkConns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false. # AuthenticateNewWorkConns specifies whether to include authentication token in new work connections sent to frps. By default, this value is false.
@@ -72,25 +73,31 @@ authenticate_new_work_conns = false
# auth token # auth token
token = 12345678 token = 12345678
# OidcClientId specifies the client ID to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". # oidc_issuer specifies the issuer to verify OIDC tokens with.
# By default, this value is "". # By default, this value is "".
oidc_client_id = oidc_issuer =
# OidcClientSecret specifies the client secret to use to get a token in OIDC authentication if AuthenticationMethod == "oidc". # oidc_audience specifies the audience OIDC tokens should contain when validated.
# By default, this value is "". # By default, this value is "".
oidc_client_secret = oidc_audience =
# OidcAudience specifies the audience of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". # oidc_skip_expiry_check specifies whether to skip checking if the OIDC token is expired.
oidc_audience = # By default, this value is false.
oidc_skip_expiry_check = false
# OidcTokenEndpointUrl specifies the URL which implements OIDC Token Endpoint.
# It will be used to get an OIDC token if AuthenticationMethod == "oidc". By default, this value is "". # oidc_skip_issuer_check specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer.
oidc_token_endpoint_url = # By default, this value is false.
oidc_skip_issuer_check = false
# heartbeat configure, it's not recommended to modify the default value # heartbeat configure, it's not recommended to modify the default value
# the default value of heartbeat_timeout is 90 # the default value of heartbeat_timeout is 90
# heartbeat_timeout = 90 # heartbeat_timeout = 90
# user_conn_timeout configure, it's not recommended to modify the default value
# the default value of user_conn_timeout is 10
# user_conn_timeout = 10
# only allow frpc to bind ports you list, if you set nothing, there won't be any limit # only allow frpc to bind ports you list, if you set nothing, there won't be any limit
allow_ports = 2000-3000,3001,3003,4000-50000 allow_ports = 2000-3000,3001,3003,4000-50000
@@ -100,7 +107,7 @@ max_pool_count = 5
# max ports can be used for each client, default value is 0 means no limit # max ports can be used for each client, default value is 0 means no limit
max_ports_per_client = 0 max_ports_per_client = 0
# TlsOnly specifies whether to only accept TLS-encrypted connections. By default, the value is false. # tls_only specifies whether to only accept TLS-encrypted connections. By default, the value is false.
tls_only = false tls_only = false
# tls_cert_file = server.crt # tls_cert_file = server.crt

View File

@@ -209,9 +209,10 @@ path = /handler
ops = NewProxy ops = NewProxy
``` ```
addr: the address where the external RPC service listens on. - addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`.
path: http request url path for the POST request. - path: http request url path for the POST request.
ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...). - ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...).
- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification.
### Metadata ### Metadata

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/fatedier/frp module github.com/fatedier/frp
go 1.12 go 1.15
require ( require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5

View File

@@ -121,11 +121,11 @@ type ClientCommonConf struct {
// HeartBeatInterval specifies at what interval heartbeats are sent to the // HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By // server, in seconds. It is not recommended to change this value. By
// default, this value is 30. // default, this value is 30.
HeartBeatInterval int64 `json:"heartbeat_interval"` HeartbeatInterval int64 `json:"heartbeat_interval"`
// HeartBeatTimeout specifies the maximum allowed heartbeat response delay // HeartBeatTimeout specifies the maximum allowed heartbeat response delay
// before the connection is terminated, in seconds. It is not recommended // before the connection is terminated, in seconds. It is not recommended
// to change this value. By default, this value is 90. // to change this value. By default, this value is 90.
HeartBeatTimeout int64 `json:"heartbeat_timeout"` HeartbeatTimeout int64 `json:"heartbeat_timeout"`
// Client meta info // Client meta info
Metas map[string]string `json:"metas"` Metas map[string]string `json:"metas"`
// UDPPacketSize specifies the udp packet size // UDPPacketSize specifies the udp packet size
@@ -160,8 +160,8 @@ func GetDefaultClientConf() ClientCommonConf {
TLSCertFile: "", TLSCertFile: "",
TLSKeyFile: "", TLSKeyFile: "",
TLSTrustedCaFile: "", TLSTrustedCaFile: "",
HeartBeatInterval: 30, HeartbeatInterval: 30,
HeartBeatTimeout: 90, HeartbeatTimeout: 90,
Metas: make(map[string]string), Metas: make(map[string]string),
UDPPacketSize: 1500, UDPPacketSize: 1500,
} }
@@ -312,7 +312,7 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error
err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout") err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout")
return return
} }
cfg.HeartBeatTimeout = v cfg.HeartbeatTimeout = v
} }
if tmpStr, ok = conf.Get("common", "heartbeat_interval"); ok { if tmpStr, ok = conf.Get("common", "heartbeat_interval"); ok {
@@ -320,7 +320,7 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error
err = fmt.Errorf("Parse conf error: invalid heartbeat_interval") err = fmt.Errorf("Parse conf error: invalid heartbeat_interval")
return return
} }
cfg.HeartBeatInterval = v cfg.HeartbeatInterval = v
} }
for k, v := range conf.Section("common") { for k, v := range conf.Section("common") {
if strings.HasPrefix(k, "meta_") { if strings.HasPrefix(k, "meta_") {
@@ -338,12 +338,12 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error
} }
func (cfg *ClientCommonConf) Check() (err error) { func (cfg *ClientCommonConf) Check() (err error) {
if cfg.HeartBeatInterval <= 0 { if cfg.HeartbeatInterval <= 0 {
err = fmt.Errorf("Parse conf error: invalid heartbeat_interval") err = fmt.Errorf("Parse conf error: invalid heartbeat_interval")
return return
} }
if cfg.HeartBeatTimeout < cfg.HeartBeatInterval { if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
return return
} }

View File

@@ -83,7 +83,7 @@ type ServerCommonConf struct {
// AssetsDir specifies the local directory that the dashboard will load // AssetsDir specifies the local directory that the dashboard will load
// resources from. If this value is "", assets will be loaded from the // resources from. If this value is "", assets will be loaded from the
// bundled executable using statik. By default, this value is "". // bundled executable using statik. By default, this value is "".
AssetsDir string `json:"asserts_dir"` AssetsDir string `json:"assets_dir"`
// LogFile specifies a file where logs will be written to. This value will // LogFile specifies a file where logs will be written to. This value will
// only be used if LogWay is set appropriately. By default, this value is // only be used if LogWay is set appropriately. By default, this value is
// "console". // "console".
@@ -154,7 +154,7 @@ type ServerCommonConf struct {
// HeartBeatTimeout specifies the maximum time to wait for a heartbeat // HeartBeatTimeout specifies the maximum time to wait for a heartbeat
// before terminating the connection. It is not recommended to change this // before terminating the connection. It is not recommended to change this
// value. By default, this value is 90. // value. By default, this value is 90.
HeartBeatTimeout int64 `json:"heart_beat_timeout"` HeartbeatTimeout int64 `json:"heartbeat_timeout"`
// UserConnTimeout specifies the maximum time to wait for a work // UserConnTimeout specifies the maximum time to wait for a work
// connection. By default, this value is 10. // connection. By default, this value is 10.
UserConnTimeout int64 `json:"user_conn_timeout"` UserConnTimeout int64 `json:"user_conn_timeout"`
@@ -199,7 +199,7 @@ func GetDefaultServerConf() ServerCommonConf {
TLSCertFile: "", TLSCertFile: "",
TLSKeyFile: "", TLSKeyFile: "",
TLSTrustedCaFile: "", TLSTrustedCaFile: "",
HeartBeatTimeout: 90, HeartbeatTimeout: 90,
UserConnTimeout: 10, UserConnTimeout: 10,
Custom404Page: "", Custom404Page: "",
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
@@ -421,7 +421,7 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error
err = fmt.Errorf("Parse conf error: heartbeat_timeout is incorrect") err = fmt.Errorf("Parse conf error: heartbeat_timeout is incorrect")
return return
} }
cfg.HeartBeatTimeout = v cfg.HeartbeatTimeout = v
} }
if tmpStr, ok = conf.Get("common", "tls_only"); ok && tmpStr == "true" { if tmpStr, ok = conf.Get("common", "tls_only"); ok && tmpStr == "true" {
@@ -458,11 +458,16 @@ func UnmarshalPluginsFromIni(sections ini.File, cfg *ServerCommonConf) {
for name, section := range sections { for name, section := range sections {
if strings.HasPrefix(name, "plugin.") { if strings.HasPrefix(name, "plugin.") {
name = strings.TrimSpace(strings.TrimPrefix(name, "plugin.")) name = strings.TrimSpace(strings.TrimPrefix(name, "plugin."))
var tls_verify, err = strconv.ParseBool(section["tls_verify"])
if err != nil {
tls_verify = true
}
options := plugin.HTTPPluginOptions{ options := plugin.HTTPPluginOptions{
Name: name, Name: name,
Addr: section["addr"], Addr: section["addr"],
Path: section["path"], Path: section["path"],
Ops: strings.Split(section["ops"], ","), Ops: strings.Split(section["ops"], ","),
TLSVerify: tls_verify,
} }
for i := range options.Ops { for i := range options.Ops {
options.Ops[i] = strings.TrimSpace(options.Ops[i]) options.Ops[i] = strings.TrimSpace(options.Ops[i])

View File

@@ -17,19 +17,22 @@ package plugin
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
"strings"
) )
type HTTPPluginOptions struct { type HTTPPluginOptions struct {
Name string Name string
Addr string Addr string
Path string Path string
Ops []string Ops []string
TLSVerify bool
} }
type httpPlugin struct { type httpPlugin struct {
@@ -40,10 +43,25 @@ type httpPlugin struct {
} }
func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin { func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin {
var url = fmt.Sprintf("%s%s", options.Addr, options.Path)
var client *http.Client
if strings.HasPrefix(url, "https://") {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: options.TLSVerify == false},
}
client = &http.Client{Transport: tr}
} else {
client = &http.Client{}
}
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
url = "http://" + url
}
return &httpPlugin{ return &httpPlugin{
options: options, options: options,
url: fmt.Sprintf("http://%s%s", options.Addr, options.Path), url: url,
client: &http.Client{}, client: client,
} }
} }

View File

@@ -15,6 +15,7 @@
package util package util
import ( import (
"net"
"net/http" "net/http"
"strings" "strings"
) )
@@ -33,6 +34,7 @@ func OkResponse() *http.Response {
return res return res
} }
// TODO: use "CanonicalHost" func to replace all "GetHostFromAddr" func.
func GetHostFromAddr(addr string) (host string) { func GetHostFromAddr(addr string) (host string) {
strs := strings.Split(addr, ":") strs := strings.Split(addr, ":")
if len(strs) > 1 { if len(strs) > 1 {
@@ -42,3 +44,34 @@ func GetHostFromAddr(addr string) (host string) {
} }
return return
} }
// canonicalHost strips port from host if present and returns the canonicalized
// host name.
func CanonicalHost(host string) (string, error) {
var err error
host = strings.ToLower(host)
if hasPort(host) {
host, _, err = net.SplitHostPort(host)
if err != nil {
return "", err
}
}
if strings.HasSuffix(host, ".") {
// Strip trailing dot from fully qualified domain names.
host = host[:len(host)-1]
}
return host, nil
}
// hasPort reports whether host contains a port number. host may be a host
// name, an IPv4 or an IPv6 address.
func hasPort(host string) bool {
colons := strings.Count(host, ":")
if colons == 0 {
return false
}
if colons == 1 {
return true
}
return host[0] == '[' && strings.Contains(host, "]:")
}

View File

@@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
var version string = "0.34.1" var version string = "0.35.0"
func Full() string { func Full() string {
return version return version

View File

@@ -17,6 +17,7 @@ package vhost
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@@ -59,20 +60,25 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
req.URL.Scheme = "http" req.URL.Scheme = "http"
url := req.Context().Value(RouteInfoURL).(string) url := req.Context().Value(RouteInfoURL).(string)
oldHost := util.GetHostFromAddr(req.Context().Value(RouteInfoHost).(string)) oldHost := util.GetHostFromAddr(req.Context().Value(RouteInfoHost).(string))
host := rp.GetRealHost(oldHost, url) rc := rp.GetRouteConfig(oldHost, url)
if host != "" { if rc != nil {
req.Host = host if rc.RewriteHost != "" {
} req.Host = rc.RewriteHost
req.URL.Host = req.Host }
// Set {domain}.{location} as URL host here to let http transport reuse connections.
req.URL.Host = rc.Domain + "." + base64.StdEncoding.EncodeToString([]byte(rc.Location))
headers := rp.GetHeaders(oldHost, url) for k, v := range rc.Headers {
for k, v := range headers { req.Header.Set(k, v)
req.Header.Set(k, v) }
} else {
req.URL.Host = req.Host
} }
}, },
Transport: &http.Transport{ Transport: &http.Transport{
ResponseHeaderTimeout: rp.responseHeaderTimeout, ResponseHeaderTimeout: rp.responseHeaderTimeout,
DisableKeepAlives: true, IdleConnTimeout: 60 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
url := ctx.Value(RouteInfoURL).(string) url := ctx.Value(RouteInfoURL).(string)
host := util.GetHostFromAddr(ctx.Value(RouteInfoHost).(string)) host := util.GetHostFromAddr(ctx.Value(RouteInfoHost).(string))
@@ -107,6 +113,14 @@ func (rp *HTTPReverseProxy) UnRegister(domain string, location string) {
rp.vhostRouter.Del(domain, location) rp.vhostRouter.Del(domain, location)
} }
func (rp *HTTPReverseProxy) GetRouteConfig(domain string, location string) *RouteConfig {
vr, ok := rp.getVhost(domain, location)
if ok {
return vr.payload.(*RouteConfig)
}
return nil
}
func (rp *HTTPReverseProxy) GetRealHost(domain string, location string) (host string) { func (rp *HTTPReverseProxy) GetRealHost(domain string, location string) (host string) {
vr, ok := rp.getVhost(domain, location) vr, ok := rp.getVhost(domain, location)
if ok { if ok {

View File

@@ -13,6 +13,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"strings" "strings"
"sync" "sync"
@@ -24,6 +25,19 @@ import (
// ReverseProxy is an HTTP Handler that takes an incoming request and // ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the // sends it to another server, proxying the response back to the
// client. // client.
//
// ReverseProxy by default sets the client IP as the value of the
// X-Forwarded-For header.
//
// If an X-Forwarded-For header already exists, the client IP is
// appended to the existing values. As a special case, if the header
// exists in the Request.Header map but has a nil value (such as when
// set by the Director func), the X-Forwarded-For header is
// not modified.
//
// To prevent IP spoofing, be sure to delete any pre-existing
// X-Forwarded-For header coming from the client or
// an untrusted proxy.
type ReverseProxy struct { type ReverseProxy struct {
// Director must be a function which modifies // Director must be a function which modifies
// the request into a new request to be sent // the request into a new request to be sent
@@ -44,9 +58,9 @@ type ReverseProxy struct {
// A negative value means to flush immediately // A negative value means to flush immediately
// after each write to the client. // after each write to the client.
// The FlushInterval is ignored when ReverseProxy // The FlushInterval is ignored when ReverseProxy
// recognizes a response as a streaming response; // recognizes a response as a streaming response, or
// for such responses, writes are flushed to the client // if its ContentLength is -1; for such responses, writes
// immediately. // are flushed to the client immediately.
FlushInterval time.Duration FlushInterval time.Duration
// ErrorLog specifies an optional logger for errors // ErrorLog specifies an optional logger for errors
@@ -97,6 +111,27 @@ func singleJoiningSlash(a, b string) string {
return a + b return a + b
} }
func joinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return singleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()
aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")
switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}
// NewSingleHostReverseProxy returns a new ReverseProxy that routes // NewSingleHostReverseProxy returns a new ReverseProxy that routes
// URLs to the scheme, host, and base path provided in target. If the // URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir", // target's path is "/base" and the incoming request was for "/dir",
@@ -109,7 +144,7 @@ func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
director := func(req *http.Request) { director := func(req *http.Request) {
req.URL.Scheme = target.Scheme req.URL.Scheme = target.Scheme
req.URL.Host = target.Host req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" { if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else { } else {
@@ -195,16 +230,19 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}() }()
} }
outreq := req.WithContext(ctx) outreq := req.Clone(ctx)
if req.ContentLength == 0 { if req.ContentLength == 0 {
outreq.Body = nil // Issue 16036: nil Body for http.Transport retries outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
} }
if outreq.Header == nil {
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
}
// ============================= // =============================
// Modified for frp // Modified for frp
outreq = outreq.WithContext(context.WithValue(outreq.Context(), RouteInfoURL, req.URL.Path)) outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoURL, req.URL.Path))
outreq = outreq.WithContext(context.WithValue(outreq.Context(), RouteInfoHost, req.Host)) outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoHost, req.Host))
outreq = outreq.WithContext(context.WithValue(outreq.Context(), RouteInfoRemote, req.RemoteAddr)) outreq = outreq.Clone(context.WithValue(outreq.Context(), RouteInfoRemote, req.RemoteAddr))
// ============================= // =============================
p.Director(outreq) p.Director(outreq)
@@ -244,10 +282,14 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// If we aren't the first proxy retain prior // If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space // X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one. // separated list and fold multiple headers into one.
if prior, ok := outreq.Header["X-Forwarded-For"]; ok { prior, ok := outreq.Header["X-Forwarded-For"]
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP clientIP = strings.Join(prior, ", ") + ", " + clientIP
} }
outreq.Header.Set("X-Forwarded-For", clientIP) if !omit {
outreq.Header.Set("X-Forwarded-For", clientIP)
}
} }
res, err := transport.RoundTrip(outreq) res, err := transport.RoundTrip(outreq)
@@ -290,7 +332,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(res.StatusCode) rw.WriteHeader(res.StatusCode)
err = p.copyResponse(rw, res.Body, p.flushInterval(req, res)) err = p.copyResponse(rw, res.Body, p.flushInterval(res))
if err != nil { if err != nil {
defer res.Body.Close() defer res.Body.Close()
// Since we're streaming the response, if we run into an error all we can do // Since we're streaming the response, if we run into an error all we can do
@@ -353,7 +395,7 @@ func shouldPanicOnCopyError(req *http.Request) bool {
func removeConnectionHeaders(h http.Header) { func removeConnectionHeaders(h http.Header) {
for _, f := range h["Connection"] { for _, f := range h["Connection"] {
for _, sf := range strings.Split(f, ",") { for _, sf := range strings.Split(f, ",") {
if sf = strings.TrimSpace(sf); sf != "" { if sf = textproto.TrimString(sf); sf != "" {
h.Del(sf) h.Del(sf)
} }
} }
@@ -362,7 +404,7 @@ func removeConnectionHeaders(h http.Header) {
// flushInterval returns the p.FlushInterval value, conditionally // flushInterval returns the p.FlushInterval value, conditionally
// overriding its value for a specific request/response. // overriding its value for a specific request/response.
func (p *ReverseProxy) flushInterval(req *http.Request, res *http.Response) time.Duration { func (p *ReverseProxy) flushInterval(res *http.Response) time.Duration {
resCT := res.Header.Get("Content-Type") resCT := res.Header.Get("Content-Type")
// For Server-Sent Events responses, flush immediately. // For Server-Sent Events responses, flush immediately.
@@ -371,7 +413,11 @@ func (p *ReverseProxy) flushInterval(req *http.Request, res *http.Response) time
return -1 // negative means immediately return -1 // negative means immediately
} }
// TODO: more specific cases? e.g. res.ContentLength == -1? // We might have the case of streaming for which Content-Length might be unset.
if res.ContentLength == -1 {
return -1
}
return p.FlushInterval return p.FlushInterval
} }
@@ -510,8 +556,6 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
return return
} }
copyHeader(res.Header, rw.Header())
hj, ok := rw.(http.Hijacker) hj, ok := rw.(http.Hijacker)
if !ok { if !ok {
p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)) p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
@@ -522,13 +566,30 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body")) p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))
return return
} }
defer backConn.Close()
backConnCloseCh := make(chan bool)
go func() {
// Ensure that the cancelation of a request closes the backend.
// See issue https://golang.org/issue/35559.
select {
case <-req.Context().Done():
case <-backConnCloseCh:
}
backConn.Close()
}()
defer close(backConnCloseCh)
conn, brw, err := hj.Hijack() conn, brw, err := hj.Hijack()
if err != nil { if err != nil {
p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err)) p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err))
return return
} }
defer conn.Close() defer conn.Close()
copyHeader(rw.Header(), res.Header)
res.Header = rw.Header()
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
if err := res.Write(brw); err != nil { if err := res.Write(brw); err != nil {
p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err)) p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err))

View File

@@ -144,7 +144,7 @@ func (v *Muxer) handle(c net.Conn) {
sConn, reqInfoMap, err := v.vhostFunc(c) sConn, reqInfoMap, err := v.vhostFunc(c)
if err != nil { if err != nil {
log.Warn("get hostname from http/https request error: %v", err) log.Debug("get hostname from http/https request error: %v", err)
c.Close() c.Close()
return return
} }

View File

@@ -408,7 +408,7 @@ func (ctl *Control) manager() {
for { for {
select { select {
case <-heartbeat.C: case <-heartbeat.C:
if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartBeatTimeout)*time.Second { if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
return return
} }

View File

@@ -74,7 +74,7 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
SubdomainHost: svr.cfg.SubDomainHost, SubdomainHost: svr.cfg.SubDomainHost,
MaxPoolCount: svr.cfg.MaxPoolCount, MaxPoolCount: svr.cfg.MaxPoolCount,
MaxPortsPerClient: svr.cfg.MaxPortsPerClient, MaxPortsPerClient: svr.cfg.MaxPortsPerClient,
HeartBeatTimeout: svr.cfg.HeartBeatTimeout, HeartBeatTimeout: svr.cfg.HeartbeatTimeout,
TotalTrafficIn: serverStats.TotalTrafficIn, TotalTrafficIn: serverStats.TotalTrafficIn,
TotalTrafficOut: serverStats.TotalTrafficOut, TotalTrafficOut: serverStats.TotalTrafficOut,

View File

@@ -100,7 +100,7 @@ func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn,
xl.Warn("failed to get work connection: %v", err) xl.Warn("failed to get work connection: %v", err)
return return
} }
xl.Info("get a new work connection: [%s]", workConn.RemoteAddr().String()) xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
xl.Spawn().AppendPrefix(pxy.GetName()) xl.Spawn().AppendPrefix(pxy.GetName())
workConn = frpNet.NewContextConn(pxy.ctx, workConn) workConn = frpNet.NewContextConn(pxy.ctx, workConn)
@@ -159,7 +159,7 @@ func (pxy *BaseProxy) startListenHandler(p Proxy, handler func(Proxy, net.Conn,
xl.Info("listener is closed") xl.Info("listener is closed")
return return
} }
xl.Debug("get a user connection [%s]", c.RemoteAddr().String()) xl.Info("get a user connection [%s]", c.RemoteAddr().String())
go handler(p, c, pxy.serverCfg) go handler(p, c, pxy.serverCfg)
} }
}(listener) }(listener)

View File

@@ -139,6 +139,7 @@ func TestHealthCheck(t *testing.T) {
} }
httpSvc3 := mock.NewHTTPServer(15005, func(w http.ResponseWriter, r *http.Request) { httpSvc3 := mock.NewHTTPServer(15005, func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second)
w.Write([]byte("http3")) w.Write([]byte("http3"))
}) })
err = httpSvc3.Start() err = httpSvc3.Start()
@@ -147,6 +148,7 @@ func TestHealthCheck(t *testing.T) {
} }
httpSvc4 := mock.NewHTTPServer(15006, func(w http.ResponseWriter, r *http.Request) { httpSvc4 := mock.NewHTTPServer(15006, func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second)
w.Write([]byte("http4")) w.Write([]byte("http4"))
}) })
err = httpSvc4.Start() err = httpSvc4.Start()
@@ -277,16 +279,30 @@ func TestHealthCheck(t *testing.T) {
// ****** load balancing type http ****** // ****** load balancing type http ******
result = make([]string, 0) result = make([]string, 0)
var wait sync.WaitGroup
var mu sync.Mutex
wait.Add(2)
code, body, _, err = util.SendHTTPMsg("GET", "http://127.0.0.1:14000/xxx", "test.balancing.com", nil, "") go func() {
assert.NoError(err) defer wait.Done()
assert.Equal(200, code) code, body, _, err := util.SendHTTPMsg("GET", "http://127.0.0.1:14000/xxx", "test.balancing.com", nil, "")
result = append(result, body) assert.NoError(err)
assert.Equal(200, code)
mu.Lock()
result = append(result, body)
mu.Unlock()
}()
code, body, _, err = util.SendHTTPMsg("GET", "http://127.0.0.1:14000/xxx", "test.balancing.com", nil, "") go func() {
assert.NoError(err) defer wait.Done()
assert.Equal(200, code) code, body, _, err = util.SendHTTPMsg("GET", "http://127.0.0.1:14000/xxx", "test.balancing.com", nil, "")
result = append(result, body) assert.NoError(err)
assert.Equal(200, code)
mu.Lock()
result = append(result, body)
mu.Unlock()
}()
wait.Wait()
assert.Contains(result, "http3") assert.Contains(result, "http3")
assert.Contains(result, "http4") assert.Contains(result, "http4")

View File

@@ -1,14 +0,0 @@
{
"presets": [
["es2015", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

5
web/frps/.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -0,0 +1,2 @@
# just a flag
ENV = 'development'

2
web/frps/.env.production Normal file
View File

@@ -0,0 +1,2 @@
# just a flag
ENV = 'production'

267
web/frps/.eslintrc.js Normal file
View File

@@ -0,0 +1,267 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue
rules: {
'vue/max-attributes-per-line': [
2,
{
singleline: 10,
multiline: {
max: 1,
allowFirstLine: false
}
}
],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
'accessor-pairs': 2,
'arrow-spacing': [
2,
{
before: true,
after: true
}
],
'block-spacing': [2, 'always'],
'brace-style': [
2,
'1tbs',
{
allowSingleLine: true
}
],
camelcase: [
0,
{
properties: 'always'
}
],
'comma-dangle': [2, 'never'],
'comma-spacing': [
2,
{
before: false,
after: true
}
],
'comma-style': [2, 'last'],
'constructor-super': 2,
curly: [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
eqeqeq: ['error', 'always', { null: 'ignore' }],
'generator-star-spacing': [
2,
{
before: true,
after: true
}
],
'handle-callback-err': [2, '^(err|error)$'],
indent: [
2,
2,
{
SwitchCase: 1
}
],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true
}
],
'keyword-spacing': [
2,
{
before: true,
after: true
}
],
'new-cap': [
2,
{
newIsCap: true,
capIsNew: false
}
],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [
2,
{
allowLoop: false,
allowSwitch: false
}
],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [
2,
{
max: 1
}
],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [
2,
{
defaultAssignment: false
}
],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [
2,
{
vars: 'all',
args: 'none'
}
],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [
2,
{
initialized: 'never'
}
],
'operator-linebreak': [
2,
'after',
{
overrides: {
'?': 'before',
':': 'before'
}
}
],
'padded-blocks': [2, 'never'],
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true
}
],
semi: [2, 'never'],
'semi-spacing': [
2,
{
before: false,
after: true
}
],
'space-before-blocks': [2, 'always'],
// 'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [
2,
{
words: true,
nonwords: false
}
],
'spaced-comment': [
2,
'always',
{
markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}
],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
yoda: [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [
2,
'always',
{
objectsInObjects: false
}
],
'array-bracket-spacing': [2, 'never']
}
}

27
web/frps/.gitignore vendored
View File

@@ -1,6 +1,25 @@
.DS_Store .DS_Store
node_modules/ node_modules
dist/ /dist
npm-debug.log
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea .idea
.vscode/settings.json .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

8
web/frps/.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"printWidth": 160,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid"
}

View File

@@ -1,7 +1,10 @@
.PHONY: dist build .PHONY: dist build
build: build: install
@npm run build @npm run build
dev: install dev: install
@npm run dev @npm run serve
install:
@npm install

25
web/frps/README.md Normal file
View File

@@ -0,0 +1,25 @@
# frps
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```

3
web/frps/babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,45 +4,43 @@
"author": "fatedier", "author": "fatedier",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "webpack-dev-server -d --inline --hot --env.dev", "serve": "vue-cli-service serve",
"build": "rimraf dist && webpack -p --progress --hide-modules" "build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^3.3.7", "core-js": "^3.7.0",
"echarts": "^3.5.0", "echarts": "^4.9.0",
"element-ui": "^2.3.8", "element-ui": "^2.14.1",
"humanize-plus": "^1.8.2", "humanize-plus": "^1.8.2",
"vue": "^2.5.16", "vue": "^2.6.12",
"vue-resource": "^1.2.1", "vue-router": "^3.4.9",
"vue-router": "^2.3.0", "vuex": "^3.5.1",
"whatwg-fetch": "^2.0.3" "whatwg-fetch": "^3.5.0"
},
"engines": {
"node": ">=6"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^6.6.0", "@vue/cli-plugin-babel": "~4.5.9",
"babel-core": "^6.21.0", "@vue/cli-plugin-eslint": "~4.5.9",
"babel-eslint": "^7.1.1", "@vue/cli-plugin-router": "~4.5.9",
"babel-loader": "^6.4.0", "@vue/cli-plugin-vuex": "~4.5.9",
"babel-plugin-component": "^1.1.1", "@vue/cli-service": "~4.5.9",
"babel-preset-es2015": "^6.13.2", "@vue/eslint-config-standard": "^5.1.2",
"css-loader": "^0.27.0", "babel-eslint": "^10.1.0",
"eslint": "^3.12.2", "eslint": "^7.14.0",
"eslint-config-enough": "^0.2.2", "eslint-plugin-import": "^2.22.1",
"eslint-loader": "^1.6.3", "eslint-plugin-node": "^11.1.0",
"file-loader": "^0.10.1", "eslint-plugin-promise": "^4.2.1",
"html-loader": "^0.4.5", "eslint-plugin-standard": "^4.1.0",
"html-webpack-plugin": "^2.24.1", "eslint-plugin-vue": "^7.1.0",
"less": "^3.0.4", "less": "^3.12.2",
"less-loader": "^4.1.0", "less-loader": "^7.1.0",
"postcss-loader": "^1.3.3", "node-sass": "^5.0.0",
"rimraf": "^2.5.4", "sass-loader": "^10.1.0",
"style-loader": "^0.13.2", "vue-template-compiler": "^2.6.12"
"url-loader": "^1.0.1", },
"vue-loader": "^15.0.10", "browserslist": [
"vue-template-compiler": "^2.1.8", "> 1%",
"webpack": "^2.2.0-rc.4", "last 2 versions",
"webpack-dev-server": "^3.1.4" "not dead"
} ]
} }

View File

@@ -1,5 +0,0 @@
module.exports = {
plugins: [
require('autoprefixer')()
]
}

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,80 +1,87 @@
<template> <template>
<div id="app"> <div id="app">
<header class="grid-content header-color"> <header class="grid-content header-color">
<el-row> <el-row>
<a class="brand" href="#">frp</a> <a class="brand" href="#">frp</a>
</el-row> </el-row>
</header> </header>
<section> <section>
<el-row :gutter="20"> <el-row>
<el-col id="side-nav" :xs="24" :md="4"> <el-col id="side-nav" :xs="24" :md="4">
<el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect"> <el-menu default-active="1" mode="vertical" theme="light" router @select="handleSelect">
<el-menu-item index="/">Overview</el-menu-item> <el-menu-item index="/">Overview</el-menu-item>
<el-submenu index="/proxies"> <el-submenu index="/proxies">
<template slot="title">Proxies</template> <template slot="title">Proxies</template>
<el-menu-item index="/proxies/tcp">TCP</el-menu-item> <el-menu-item index="/proxies/tcp">TCP</el-menu-item>
<el-menu-item index="/proxies/udp">UDP</el-menu-item> <el-menu-item index="/proxies/udp">UDP</el-menu-item>
<el-menu-item index="/proxies/http">HTTP</el-menu-item> <el-menu-item index="/proxies/http">HTTP</el-menu-item>
<el-menu-item index="/proxies/https">HTTPS</el-menu-item> <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
<el-menu-item index="/proxies/stcp">STCP</el-menu-item> <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
</el-submenu> </el-submenu>
<el-menu-item index="">Help</el-menu-item> <el-menu-item index="">Help</el-menu-item>
</el-menu> </el-menu>
</el-col> </el-col>
<el-col :xs="24" :md="20"> <el-col :xs="24" :md="20">
<div id="content"> <div id="content">
<router-view></router-view> <router-view v-if="serverInfo" />
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</section> </section>
<footer></footer> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
methods: { computed: {
handleSelect(key, path) { serverInfo() {
if (key == '') { return this.$store.state.serverInfo
window.open("https://github.com/fatedier/frp")
}
}
}
} }
},
async created() {
this.$store.dispatch('fetchServerInfo')
},
methods: {
handleSelect(key, path) {
if (key === '') {
window.open('https://github.com/fatedier/frp')
}
}
}
}
</script> </script>
<style> <style>
body { body {
background-color: #fafafa; background-color: #fafafa;
margin: 0px; margin: 0px;
font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif; font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
} }
header { header {
width: 100%; width: 100%;
height: 60px; height: 60px;
} }
.header-color { .header-color {
background: #58B7FF; background: #58b7ff;
} }
#content { #content {
margin-top: 20px; margin-top: 20px;
padding-right: 40px; padding-right: 40px;
} }
.brand { .brand {
color: #fff; color: #fff;
background-color: transparent; background-color: transparent;
margin-left: 20px; margin-left: 20px;
float: left; float: left;
line-height: 25px; line-height: 25px;
font-size: 25px; font-size: 25px;
padding: 15px 15px; padding: 15px 15px;
height: 30px; height: 30px;
text-decoration: none; text-decoration: none;
} }
</style> </style>

View File

@@ -1,166 +1,160 @@
<template> <template>
<div> <div>
<el-row> <el-row>
<el-col :md="12"> <el-col :md="12">
<div class="source"> <div class="source">
<el-form label-position="left" class="server_info"> <el-form label-position="left" class="server_info">
<el-form-item label="Version"> <el-form-item label="Version">
<span>{{ version }}</span> <span>{{ version }}</span>
</el-form-item> </el-form-item>
<el-form-item label="BindPort"> <el-form-item label="BindPort">
<span>{{ bind_port }}</span> <span>{{ bind_port }}</span>
</el-form-item> </el-form-item>
<el-form-item label="BindUdpPort"> <el-form-item label="BindUdpPort">
<span>{{ bind_udp_port }}</span> <span>{{ bind_udp_port }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Http Port"> <el-form-item label="Http Port">
<span>{{ vhost_http_port }}</span> <span>{{ vhost_http_port }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Https Port"> <el-form-item label="Https Port">
<span>{{ vhost_https_port }}</span> <span>{{ vhost_https_port }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Subdomain Host"> <el-form-item label="Subdomain Host">
<span>{{ subdomain_host }}</span> <span>{{ subdomain_host }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Max PoolCount"> <el-form-item label="Max PoolCount">
<span>{{ max_pool_count }}</span> <span>{{ max_pool_count }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Max Ports Per Client"> <el-form-item label="Max Ports Per Client">
<span>{{ max_ports_per_client }}</span> <span>{{ max_ports_per_client }}</span>
</el-form-item> </el-form-item>
<el-form-item label="HeartBeat Timeout"> <el-form-item label="HeartBeat Timeout">
<span>{{ heart_beat_timeout }}</span> <span>{{ heart_beat_timeout }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Client Counts"> <el-form-item label="Client Counts">
<span>{{ client_counts }}</span> <span>{{ client_counts }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Current Connections"> <el-form-item label="Current Connections">
<span>{{ cur_conns }}</span> <span>{{ cur_conns }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Proxy Counts"> <el-form-item label="Proxy Counts">
<span>{{ proxy_counts }}</span> <span>{{ proxy_counts }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</el-col> </el-col>
<el-col :md="12"> <el-col :md="12">
<div id="traffic" style="width: 400px;height:250px;margin-bottom: 30px;"></div> <div id="traffic" style="width: 400px; height: 250px; margin-bottom: 30px" />
<div id="proxies" style="width: 400px;height:250px;"></div> <div id="proxies" style="width: 400px; height: 250px" />
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</template> </template>
<script> <script>
import {DrawTrafficChart, DrawProxyChart} from '../utils/chart.js' import { DrawTrafficChart, DrawProxyChart } from '../utils/chart.js'
export default { export default {
data() { data() {
return { return {
version: '', version: '',
bind_port: '', bind_port: '',
bind_udp_port: '', bind_udp_port: '',
vhost_http_port: '', vhost_http_port: '',
vhost_https_port: '', vhost_https_port: '',
subdomain_host: '', subdomain_host: '',
max_pool_count: '', max_pool_count: '',
max_ports_per_client: '', max_ports_per_client: '',
heart_beat_timeout: '', heart_beat_timeout: '',
client_counts: '', client_counts: '',
cur_conns: '', cur_conns: '',
proxy_counts: '' proxy_counts: ''
}
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData() {
fetch('/api/serverinfo', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.version = json.version
this.bind_port = json.bind_port
this.bind_udp_port = json.bind_udp_port
if (this.bind_udp_port == 0) {
this.bind_udp_port = "disable"
}
this.vhost_http_port = json.vhost_http_port
if (this.vhost_http_port == 0) {
this.vhost_http_port = "disable"
}
this.vhost_https_port = json.vhost_https_port
if (this.vhost_https_port == 0) {
this.vhost_https_port = "disable"
}
this.subdomain_host = json.subdomain_host
this.max_pool_count = json.max_pool_count
this.max_ports_per_client = json.max_ports_per_client
if (this.max_ports_per_client == 0) {
this.max_ports_per_client = "no limit"
}
this.heart_beat_timeout = json.heart_beat_timeout
this.client_counts = json.client_counts
this.cur_conns = json.cur_conns
this.proxy_counts = 0
if (json.proxy_type_count != null) {
if (json.proxy_type_count.tcp != null) {
this.proxy_counts += json.proxy_type_count.tcp
}
if (json.proxy_type_count.udp != null) {
this.proxy_counts += json.proxy_type_count.udp
}
if (json.proxy_type_count.http != null) {
this.proxy_counts += json.proxy_type_count.http
}
if (json.proxy_type_count.https != null) {
this.proxy_counts += json.proxy_type_count.https
}
if (json.proxy_type_count.stcp != null) {
this.proxy_counts += json.proxy_type_count.stcp
}
if (json.proxy_type_count.xtcp != null) {
this.proxy_counts += json.proxy_type_count.xtcp
}
}
DrawTrafficChart('traffic', json.total_traffic_in, json.total_traffic_out)
DrawProxyChart('proxies', json)
}).catch( err => {
this.$message({
showClose: true,
message: 'Get server info from frps failed!',
type: 'warning'
})
})
}
}
} }
},
computed: {
serverInfo() {
return this.$store.state.serverInfo
}
},
mounted() {
this.initData()
},
methods: {
initData() {
console.log(!!this.serverInfo, this.serverInfo)
if (!this.serverInfo) return
this.version = this.serverInfo.version
this.bind_port = this.serverInfo.bind_port
this.bind_udp_port = this.serverInfo.bind_udp_port
if (this.bind_udp_port === 0) {
this.bind_udp_port = 'disable'
}
this.vhost_http_port = this.serverInfo.vhost_http_port
if (this.vhost_http_port === 0) {
this.vhost_http_port = 'disable'
}
this.vhost_https_port = this.serverInfo.vhost_https_port
if (this.vhost_https_port === 0) {
this.vhost_https_port = 'disable'
}
this.subdomain_host = this.serverInfo.subdomain_host
this.max_pool_count = this.serverInfo.max_pool_count
this.max_ports_per_client = this.serverInfo.max_ports_per_client
if (this.max_ports_per_client === 0) {
this.max_ports_per_client = 'no limit'
}
this.heart_beat_timeout = this.serverInfo.heart_beat_timeout
this.client_counts = this.serverInfo.client_counts
this.cur_conns = this.serverInfo.cur_conns
this.proxy_counts = 0
if (this.serverInfo.proxy_type_count != null) {
if (this.serverInfo.proxy_type_count.tcp != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.tcp
}
if (this.serverInfo.proxy_type_count.udp != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.udp
}
if (this.serverInfo.proxy_type_count.http != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.http
}
if (this.serverInfo.proxy_type_count.https != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.https
}
if (this.serverInfo.proxy_type_count.stcp != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.stcp
}
if (this.serverInfo.proxy_type_count.xtcp != null) {
this.proxy_counts += this.serverInfo.proxy_type_count.xtcp
}
}
DrawTrafficChart('traffic', this.serverInfo.total_traffic_in, this.serverInfo.total_traffic_out)
DrawProxyChart('proxies', this.serverInfo)
}
}
}
</script> </script>
<style> <style>
.source { .source {
border: 1px solid #eaeefb; border: 1px solid #eaeefb;
border-radius: 4px; border-radius: 4px;
transition: .2s; transition: 0.2s;
padding: 24px; padding: 24px;
} }
.server_info { .server_info {
margin-left: 40px; margin-left: 40px;
font-size: 0px; font-size: 0px;
} }
.server_info label { .server_info label {
width: 150px; width: 150px;
color: #99a9bf; color: #99a9bf;
} }
.server_info .el-form-item { .server_info .el-form-item {
margin-right: 0; margin-right: 0;
margin-bottom: 0; margin-bottom: 0;
width: 100%; width: 100%;
} }
</style> </style>

View File

@@ -1,19 +1,14 @@
<template> <template>
<div> <div>
<el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%"> <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
<el-table-column type="expand"> <el-table-column type="expand">
<template slot-scope="props"> <template slot-scope="props">
<el-popover <el-popover ref="popover4" placement="right" width="600" style="margin-left: 0px" trigger="click">
ref="popover4" <my-traffic-chart :proxy-name="props.row.name" />
placement="right"
width="600"
style="margin-left:0px"
trigger="click">
<my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
</el-popover> </el-popover>
<el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button> <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom: 10px">Traffic Statistics</el-button>
<el-form label-position="left" inline class="demo-table-expand"> <el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="Name"> <el-form-item label="Name">
<span>{{ props.row.name }}</span> <span>{{ props.row.name }}</span>
@@ -45,104 +40,68 @@
<el-form-item label="Last Close"> <el-form-item label="Last Close">
<span>{{ props.row.last_close_time }}</span> <span>{{ props.row.last_close_time }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="Name" prop="name" sortable />
label="Name" <el-table-column label="Port" prop="port" sortable />
prop="name" <el-table-column label="Connections" prop="conns" sortable />
sortable> <el-table-column label="Traffic In" prop="traffic_in" :formatter="formatTrafficIn" sortable />
</el-table-column> <el-table-column label="Traffic Out" prop="traffic_out" :formatter="formatTrafficOut" sortable />
<el-table-column <el-table-column label="status" prop="status" sortable>
label="Port" <template slot-scope="scope">
prop="port" <el-tag v-if="scope.row.status === 'online'" type="success">{{ scope.row.status }}</el-tag>
sortable> <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</el-table-column> </template>
<el-table-column </el-table-column>
label="Connections" </el-table>
prop="conns" </div>
sortable>
</el-table-column>
<el-table-column
label="Traffic In"
prop="traffic_in"
:formatter="formatTrafficIn"
sortable>
</el-table-column>
<el-table-column
label="Traffic Out"
prop="traffic_out"
:formatter="formatTrafficOut"
sortable>
</el-table-column>
<el-table-column
label="status"
prop="status"
sortable>
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
<el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
<script> <script>
import Humanize from 'humanize-plus'; import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from './Traffic.vue'
import { import { HttpProxy } from '../utils/proxy.js'
HttpProxy export default {
} from '../utils/proxy.js' components: {
export default { 'my-traffic-chart': Traffic
data() { },
return { data() {
proxies: null, return {
vhost_http_port: "", proxies: [],
subdomain_host: "" vhost_http_port: '',
subdomain_host: ''
}
},
computed: {
serverInfo() {
return this.$store.state.serverInfo
}
},
mounted() {
this.initData()
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
async initData() {
if (!this.serverInfo) return
this.vhost_http_port = this.serverInfo.vhost_http_port
this.subdomain_host = this.serverInfo.subdomain_host
if (this.vhost_http_port == null || this.vhost_http_port === 0) return
const json = await this.$fetch('proxy/http')
if (!json) return
this.proxies = []
for (const proxyStats of json.proxies) {
this.proxies.push(new HttpProxy(proxyStats, this.vhost_http_port, this.subdomain_host))
} }
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
fetchData() {
fetch('/api/serverinfo', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.vhost_http_port = json.vhost_http_port
this.subdomain_host = json.subdomain_host
if (this.vhost_http_port == null || this.vhost_http_port == 0) {
return
} else {
fetch('/api/proxy/http', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.proxies = new Array()
for (let proxyStats of json.proxies) {
this.proxies.push(new HttpProxy(proxyStats, this.vhost_http_port, this.subdomain_host))
}
})
}
})
}
},
components: {
'my-traffic-chart': Traffic
} }
} }
}
</script> </script>
<style>
</style>

View File

@@ -1,19 +1,14 @@
<template> <template>
<div> <div>
<el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%"> <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
<el-table-column type="expand"> <el-table-column type="expand">
<template slot-scope="props"> <template slot-scope="props">
<el-popover <el-popover ref="popover4" placement="right" width="600" style="margin-left: 0px" trigger="click">
ref="popover4" <my-traffic-chart :proxy-name="props.row.name" />
placement="right"
width="600"
style="margin-left:0px"
trigger="click">
<my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
</el-popover> </el-popover>
<el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button> <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom: 10px">Traffic Statistics</el-button>
<el-form label-position="left" inline class="demo-table-expand"> <el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="Name"> <el-form-item label="Name">
<span>{{ props.row.name }}</span> <span>{{ props.row.name }}</span>
@@ -39,105 +34,69 @@
<el-form-item label="Last Close"> <el-form-item label="Last Close">
<span>{{ props.row.last_close_time }}</span> <span>{{ props.row.last_close_time }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="Name" prop="name" sortable />
label="Name" <el-table-column label="Port" prop="port" sortable />
prop="name" <el-table-column label="Connections" prop="conns" sortable />
sortable> <el-table-column label="Traffic In" prop="traffic_in" :formatter="formatTrafficIn" sortable />
</el-table-column> <el-table-column label="Traffic Out" prop="traffic_out" :formatter="formatTrafficOut" sortable />
<el-table-column <el-table-column label="status" prop="status" sortable>
label="Port" <template slot-scope="scope">
prop="port" <el-tag v-if="scope.row.status === 'online'" type="success">{{ scope.row.status }}</el-tag>
sortable> <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</el-table-column> </template>
<el-table-column </el-table-column>
label="Connections" </el-table>
prop="conns" </div>
sortable>
</el-table-column>
<el-table-column
label="Traffic In"
prop="traffic_in"
:formatter="formatTrafficIn"
sortable>
</el-table-column>
<el-table-column
label="Traffic Out"
prop="traffic_out"
:formatter="formatTrafficOut"
sortable>
</el-table-column>
<el-table-column
label="status"
prop="status"
sortable>
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
<el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
<script> <script>
import Humanize from 'humanize-plus'; import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from './Traffic.vue'
import { import { HttpsProxy } from '../utils/proxy.js'
HttpsProxy export default {
} from '../utils/proxy.js' components: {
export default { 'my-traffic-chart': Traffic
data() { },
return { data() {
proxies: null, return {
vhost_https_port: '', proxies: [],
subdomain_host: '' vhost_https_port: '',
subdomain_host: ''
}
},
computed: {
serverInfo() {
return this.$store.state.serverInfo
}
},
mounted() {
this.initData()
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
async initData() {
if (!this.serverInfo) return
this.vhost_https_port = this.serverInfo.vhost_https_port
this.subdomain_host = this.serverInfo.subdomain_host
if (this.vhost_https_port == null || this.vhost_https_port === 0) return
const json = await this.$fetch('proxy/https')
if (!json) return
this.proxies = []
for (const proxyStats of json.proxies) {
this.proxies.push(new HttpsProxy(proxyStats, this.vhost_https_port, this.subdomain_host))
} }
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
fetchData() {
fetch('/api/serverinfo', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.vhost_https_port = json.vhost_https_port
this.subdomain_host = json.subdomain_host
if (this.vhost_https_port == null || this.vhost_https_port == 0) {
return
} else {
fetch('/api/proxy/https', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.proxies = new Array()
for (let proxyStats of json.proxies) {
this.proxies.push(new HttpsProxy(proxyStats, this.vhost_https_port, this.subdomain_host))
}
})
}
})
}
},
components: {
'my-traffic-chart': Traffic
} }
} }
}
</script> </script>
<style>
</style>

View File

@@ -1,19 +1,16 @@
<template> <template>
<div> <div>
<el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%"> <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
<el-table-column type="expand"> <el-table-column type="expand">
<template slot-scope="props"> <template slot-scope="props">
<el-popover <el-popover ref="popover4" placement="right" width="600" style="margin-left: 0px" trigger="click">
ref="popover4" <my-traffic-chart :proxy-name="props.row.name" />
placement="right"
width="600"
style="margin-left:0px"
trigger="click">
<my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
</el-popover> </el-popover>
<el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button> <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom: 10px" @click="fetchData2">
Traffic Statistics
</el-button>
<el-form label-position="left" inline class="demo-table-expand"> <el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="Name"> <el-form-item label="Name">
<span>{{ props.row.name }}</span> <span>{{ props.row.name }}</span>
@@ -33,83 +30,57 @@
<el-form-item label="Last Close"> <el-form-item label="Last Close">
<span>{{ props.row.last_close_time }}</span> <span>{{ props.row.last_close_time }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="Name" prop="name" sortable />
label="Name" <el-table-column label="Connections" prop="conns" sortable />
prop="name" <el-table-column label="Traffic In" prop="traffic_in" :formatter="formatTrafficIn" sortable />
sortable> <el-table-column label="Traffic Out" prop="traffic_out" :formatter="formatTrafficOut" sortable />
</el-table-column> <el-table-column label="status" prop="status" sortable>
<el-table-column <template slot-scope="scope">
label="Connections" <el-tag v-if="scope.row.status === 'online'" type="success">{{ scope.row.status }}</el-tag>
prop="conns" <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
sortable> </template>
</el-table-column> </el-table-column>
<el-table-column </el-table>
label="Traffic In" </div>
prop="traffic_in"
:formatter="formatTrafficIn"
sortable>
</el-table-column>
<el-table-column
label="Traffic Out"
prop="traffic_out"
:formatter="formatTrafficOut"
sortable>
</el-table-column>
<el-table-column
label="status"
prop="status"
sortable>
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
<el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from './Traffic.vue'
import { StcpProxy } from '../utils/proxy.js' import { StcpProxy } from '../utils/proxy.js'
export default { export default {
data() { components: {
return { 'my-traffic-chart': Traffic
proxies: null },
data() {
return {
proxies: []
}
},
mounted() {
this.initData()
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
async initData() {
const json = await this.$fetch('proxy/stcp')
if (!json) return
this.proxies = []
for (const proxyStats of json.proxies) {
this.proxies.push(new StcpProxy(proxyStats))
} }
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
fetchData() {
fetch('/api/proxy/stcp', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.proxies = new Array()
for (let proxyStats of json.proxies) {
this.proxies.push(new StcpProxy(proxyStats))
}
})
}
},
components: {
'my-traffic-chart': Traffic
} }
} }
}
</script> </script>
<style> <style>

View File

@@ -1,19 +1,16 @@
<template> <template>
<div> <div>
<el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%"> <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
<el-table-column type="expand"> <el-table-column type="expand">
<template slot-scope="props"> <template slot-scope="props">
<el-popover <el-popover placement="right" width="600" style="margin-left: 0px" trigger="click">
ref="popover4" <my-traffic-chart :proxy-name="props.row.name" />
placement="right"
width="600" <el-button slot="reference" type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom: 10px">
style="margin-left:0px" Traffic Statistics
trigger="click"> </el-button>
<my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
</el-popover> </el-popover>
<el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
<el-form label-position="left" inline class="demo-table-expand"> <el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="Name"> <el-form-item label="Name">
<span>{{ props.row.name }}</span> <span>{{ props.row.name }}</span>
@@ -36,88 +33,58 @@
<el-form-item label="Last Close"> <el-form-item label="Last Close">
<span>{{ props.row.last_close_time }}</span> <span>{{ props.row.last_close_time }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="Name" prop="name" sortable />
label="Name" <el-table-column label="Port" prop="port" sortable />
prop="name" <el-table-column label="Connections" prop="conns" sortable />
sortable> <el-table-column label="Traffic In" prop="traffic_in" :formatter="formatTrafficIn" sortable />
</el-table-column> <el-table-column label="Traffic Out" prop="traffic_out" :formatter="formatTrafficOut" sortable />
<el-table-column <el-table-column label="status" prop="status" sortable>
label="Port" <template slot-scope="scope">
prop="port" <el-tag v-if="scope.row.status === 'online'" type="success">{{ scope.row.status }}</el-tag>
sortable> <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</el-table-column> </template>
<el-table-column </el-table-column>
label="Connections" </el-table>
prop="conns" </div>
sortable>
</el-table-column>
<el-table-column
label="Traffic In"
prop="traffic_in"
:formatter="formatTrafficIn"
sortable>
</el-table-column>
<el-table-column
label="Traffic Out"
prop="traffic_out"
:formatter="formatTrafficOut"
sortable>
</el-table-column>
<el-table-column
label="status"
prop="status"
sortable>
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
<el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
<script> <script>
import Humanize from 'humanize-plus' import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from './Traffic.vue'
import { TcpProxy } from '../utils/proxy.js' import { TcpProxy } from '../utils/proxy.js'
export default { export default {
data() { components: {
return { 'my-traffic-chart': Traffic
proxies: null },
data() {
return {
proxies: []
}
},
mounted() {
this.initData()
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
async initData() {
const json = await this.$fetch('proxy/tcp')
if (!json) return
this.proxies = []
for (const proxyStats of json.proxies) {
this.proxies.push(new TcpProxy(proxyStats))
} }
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
fetchData() {
fetch('/api/proxy/tcp', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.proxies = new Array()
for (let proxyStats of json.proxies) {
this.proxies.push(new TcpProxy(proxyStats))
}
})
}
},
components: {
'my-traffic-chart': Traffic
} }
} }
}
</script> </script>
<style> <style>

View File

@@ -1,19 +1,14 @@
<template> <template>
<div> <div>
<el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%"> <el-table :data="proxies" :default-sort="{ prop: 'name', order: 'ascending' }" style="width: 100%">
<el-table-column type="expand"> <el-table-column type="expand">
<template slot-scope="props"> <template slot-scope="props">
<el-popover <el-popover ref="popover4" placement="right" width="600" style="margin-left: 0px" trigger="click">
ref="popover4" <my-traffic-chart :proxy-name="props.row.name" />
placement="right"
width="600"
style="margin-left:0px"
trigger="click">
<my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
</el-popover> </el-popover>
<el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button> <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom: 10px">Traffic Statistics</el-button>
<el-form label-position="left" inline class="demo-table-expand"> <el-form label-position="left" inline class="demo-table-expand">
<el-form-item label="Name"> <el-form-item label="Name">
<span>{{ props.row.name }}</span> <span>{{ props.row.name }}</span>
@@ -36,91 +31,57 @@
<el-form-item label="Last Close"> <el-form-item label="Last Close">
<span>{{ props.row.last_close_time }}</span> <span>{{ props.row.last_close_time }}</span>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="Name" prop="name" sortable />
label="Name" <el-table-column label="Port" prop="port" sortable />
prop="name" <el-table-column label="Connections" prop="conns" sortable />
sortable> <el-table-column label="Traffic In" prop="traffic_in" :formatter="formatTrafficIn" sortable />
</el-table-column> <el-table-column label="Traffic Out" prop="traffic_out" :formatter="formatTrafficOut" sortable />
<el-table-column <el-table-column label="status" prop="status" sortable>
label="Port" <template slot-scope="scope">
prop="port" <el-tag v-if="scope.row.status === 'online'" type="success">{{ scope.row.status }}</el-tag>
sortable> <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</el-table-column> </template>
<el-table-column </el-table-column>
label="Connections" </el-table>
prop="conns" </div>
sortable>
</el-table-column>
<el-table-column
label="Traffic In"
prop="traffic_in"
:formatter="formatTrafficIn"
sortable>
</el-table-column>
<el-table-column
label="Traffic Out"
prop="traffic_out"
:formatter="formatTrafficOut"
sortable>
</el-table-column>
<el-table-column
label="status"
prop="status"
sortable>
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
<el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
<script> <script>
import Humanize from 'humanize-plus'; import Humanize from 'humanize-plus'
import Traffic from './Traffic.vue' import Traffic from './Traffic.vue'
import { import { UdpProxy } from '../utils/proxy.js'
UdpProxy export default {
} from '../utils/proxy.js' components: {
export default { 'my-traffic-chart': Traffic
data() { },
return { data() {
proxies: null return {
proxies: []
}
},
mounted() {
this.initData()
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
async initData() {
const json = await this.$fetch('proxy/udp')
if (!json) return
this.proxies = []
for (const proxyStats of json.proxies) {
this.proxies.push(new UdpProxy(proxyStats))
} }
},
created() {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
formatTrafficIn(row, column) {
return Humanize.fileSize(row.traffic_in)
},
formatTrafficOut(row, column) {
return Humanize.fileSize(row.traffic_out)
},
fetchData() {
fetch('/api/proxy/udp', {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
this.proxies = new Array()
for (let proxyStats of json.proxies) {
this.proxies.push(new UdpProxy(proxyStats))
}
})
}
},
components: {
'my-traffic-chart': Traffic
} }
} }
}
</script> </script>
<style>
</style>

View File

@@ -1,36 +1,26 @@
<template> <template>
<div :id="proxy_name" style="width: 600px;height:400px;"></div> <div :id="proxyName" style="width: 600px; height: 400px" />
</template> </template>
<script> <script>
import {DrawProxyTrafficChart} from '../utils/chart.js' import { DrawProxyTrafficChart } from '../utils/chart.js'
export default { export default {
props: ['proxy_name'], props: {
created() { proxyName: {
this.fetchData() type: String,
}, required: true
//watch: {
//'$route': 'fetchData'
//},
methods: {
fetchData() {
let url = '/api/traffic/' + this.proxy_name
fetch(url, {credentials: 'include'})
.then(res => {
return res.json()
}).then(json => {
DrawProxyTrafficChart(this.proxy_name, json.traffic_in, json.traffic_out)
}).catch( err => {
this.$message({
showClose: true,
message: 'Get server info from frps failed!' + err,
type: 'warning'
})
})
}
} }
},
mounted() {
this.initData()
},
methods: {
async initData() {
const json = await this.$fetch(`traffic/${this.proxyName}`)
if (!json) return
DrawProxyTrafficChart(this.proxyName, json.traffic_in, json.traffic_out)
}
}
} }
</script> </script>
<style>
</style>

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
</head>
<body>
<div id="app"></div>
<!--<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>-->
<!--<script src="//cdn.bootcss.com/echarts/3.4.0/echarts.min.js"></script>-->
</body>
</html>

View File

@@ -1,19 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
//import ElementUI from 'element-ui' // import ElementUI from 'element-ui'
import { import { Button, Form, FormItem, Row, Col, Table, TableColumn, Popover, Menu, Submenu, MenuItem, Tag, Message } from 'element-ui'
Button,
Form,
FormItem,
Row,
Col,
Table,
TableColumn,
Popover,
Menu,
Submenu,
MenuItem,
Tag
} from 'element-ui'
import lang from 'element-ui/lib/locale/lang/en' import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale' import locale from 'element-ui/lib/locale'
import 'element-ui/lib/theme-chalk/index.css' import 'element-ui/lib/theme-chalk/index.css'
@@ -21,6 +8,7 @@ import './utils/less/custom.less'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import store from '@/store'
import 'whatwg-fetch' import 'whatwg-fetch'
locale.use(lang) locale.use(lang)
@@ -37,12 +25,15 @@ Vue.use(Menu)
Vue.use(Submenu) Vue.use(Submenu)
Vue.use(MenuItem) Vue.use(MenuItem)
Vue.use(Tag) Vue.use(Tag)
Vue.prototype.$message = Message
import fetch from '@/utils/fetch'
Vue.prototype.$fetch = fetch
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({
el: '#app', router,
router, store,
template: '<App/>', render: h => h(App)
components: { App } }).$mount('#app')
})

View File

@@ -10,29 +10,36 @@ import ProxiesStcp from '../components/ProxiesStcp.vue'
Vue.use(Router) Vue.use(Router)
export default new Router({ export default new Router({
routes: [{ routes: [
path: '/', {
name: 'Overview', path: '/',
component: Overview name: 'Overview',
}, { component: Overview
path: '/proxies/tcp', },
name: 'ProxiesTcp', {
component: ProxiesTcp path: '/proxies/tcp',
}, { name: 'ProxiesTcp',
path: '/proxies/udp', component: ProxiesTcp
name: 'ProxiesUdp', },
component: ProxiesUdp {
}, { path: '/proxies/udp',
path: '/proxies/http', name: 'ProxiesUdp',
name: 'ProxiesHttp', component: ProxiesUdp
component: ProxiesHttp },
}, { {
path: '/proxies/https', path: '/proxies/http',
name: 'ProxiesHttps', name: 'ProxiesHttp',
component: ProxiesHttps component: ProxiesHttp
}, { },
path: '/proxies/stcp', {
name: 'ProxiesStcp', path: '/proxies/https',
component: ProxiesStcp name: 'ProxiesHttps',
}] component: ProxiesHttps
},
{
path: '/proxies/stcp',
name: 'ProxiesStcp',
component: ProxiesStcp
}
]
}) })

View File

@@ -0,0 +1,24 @@
import Vue from 'vue'
import Vuex from 'vuex'
import fetch from '@/utils/fetch'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
serverInfo: null
},
mutations: {
SET_SERVER_INFO(state, serverInfo) {
state.serverInfo = serverInfo
}
},
actions: {
async fetchServerInfo({ commit }) {
const json = await fetch('serverinfo')
commit('SET_SERVER_INFO', json || null)
return json
}
}
})
export default store

View File

@@ -1,199 +1,215 @@
import Humanize from "humanize-plus" import Humanize from 'humanize-plus'
import echarts from "echarts/lib/echarts" import echarts from 'echarts/lib/echarts'
import "echarts/theme/macarons" import 'echarts/theme/macarons'
import "echarts/lib/chart/bar" import 'echarts/lib/chart/bar'
import "echarts/lib/chart/pie" import 'echarts/lib/chart/pie'
import "echarts/lib/component/tooltip" import 'echarts/lib/component/tooltip'
import "echarts/lib/component/title" import 'echarts/lib/component/title'
function DrawTrafficChart(elementId, trafficIn, trafficOut) { function DrawTrafficChart(elementId, trafficIn, trafficOut) {
let myChart = echarts.init(document.getElementById(elementId), 'macarons'); const myChart = echarts.init(document.getElementById(elementId), 'macarons')
myChart.showLoading() myChart.showLoading()
let option = { const option = {
title: { title: {
text: 'Network Traffic', text: 'Network Traffic',
subtext: 'today', subtext: 'today',
x: 'center' x: 'center'
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: function(v) { formatter: function(v) {
return Humanize.fileSize(v.data.value) + " (" + v.percent + "%)" return Humanize.fileSize(v.data.value) + ' (' + v.percent + '%)'
} }
}, },
series: [{ series: [
type: 'pie', {
radius: '55%', type: 'pie',
center: ['50%', '60%'], radius: '55%',
data: [{ center: ['50%', '60%'],
value: trafficIn, data: [
name: 'Traffic In' {
}, { value: trafficIn,
value: trafficOut, name: 'Traffic In'
name: 'Traffic Out' },
}, ], {
itemStyle: { value: trafficOut,
emphasis: { name: 'Traffic Out'
shadowBlur: 10, }
shadowOffsetX: 0, ],
shadowColor: 'rgba(0, 0, 0, 0.5)' itemStyle: {
} emphasis: {
} shadowBlur: 10,
}] shadowOffsetX: 0,
}; shadowColor: 'rgba(0, 0, 0, 0.5)'
myChart.setOption(option); }
myChart.hideLoading() }
}
]
}
myChart.setOption(option)
myChart.hideLoading()
} }
function DrawProxyChart(elementId, serverInfo) { function DrawProxyChart(elementId, serverInfo) {
if (serverInfo.proxy_type_count.tcp == null) { if (serverInfo.proxy_type_count.tcp == null) {
serverInfo.proxy_type_count.tcp = 0 serverInfo.proxy_type_count.tcp = 0
} }
if (serverInfo.proxy_type_count.udp == null) { if (serverInfo.proxy_type_count.udp == null) {
serverInfo.proxy_type_count.udp = 0 serverInfo.proxy_type_count.udp = 0
} }
if (serverInfo.proxy_type_count.http == null) { if (serverInfo.proxy_type_count.http == null) {
serverInfo.proxy_type_count.http = 0 serverInfo.proxy_type_count.http = 0
} }
if (serverInfo.proxy_type_count.https == null) { if (serverInfo.proxy_type_count.https == null) {
serverInfo.proxy_type_count.https = 0 serverInfo.proxy_type_count.https = 0
} }
if (serverInfo.proxy_type_count.stcp == null) { if (serverInfo.proxy_type_count.stcp == null) {
serverInfo.proxy_type_count.stcp = 0 serverInfo.proxy_type_count.stcp = 0
} }
if (serverInfo.proxy_type_count.xtcp == null) { if (serverInfo.proxy_type_count.xtcp == null) {
serverInfo.proxy_type_count.xtcp = 0 serverInfo.proxy_type_count.xtcp = 0
} }
let myChart = echarts.init(document.getElementById(elementId), 'macarons') const myChart = echarts.init(document.getElementById(elementId), 'macarons')
myChart.showLoading() myChart.showLoading()
let option = { const option = {
title: { title: {
text: 'Proxies', text: 'Proxies',
subtext: 'now', subtext: 'now',
x: 'center' x: 'center'
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: function(v) { formatter: function(v) {
return v.data.value return v.data.value
} }
}, },
series: [{ series: [
type: 'pie', {
radius: '55%', type: 'pie',
center: ['50%', '60%'], radius: '55%',
data: [{ center: ['50%', '60%'],
value: serverInfo.proxy_type_count.tcp, data: [
name: 'TCP' {
}, { value: serverInfo.proxy_type_count.tcp,
value: serverInfo.proxy_type_count.udp, name: 'TCP'
name: 'UDP' },
}, { {
value: serverInfo.proxy_type_count.http, value: serverInfo.proxy_type_count.udp,
name: 'HTTP' name: 'UDP'
}, { },
value: serverInfo.proxy_type_count.https, {
name: 'HTTPS' value: serverInfo.proxy_type_count.http,
}, { name: 'HTTP'
value: serverInfo.proxy_type_count.stcp, },
name: 'STCP' {
}, { value: serverInfo.proxy_type_count.https,
value: serverInfo.proxy_type_count.xtcp, name: 'HTTPS'
name: 'XTCP' },
}], {
itemStyle: { value: serverInfo.proxy_type_count.stcp,
emphasis: { name: 'STCP'
shadowBlur: 10, },
shadowOffsetX: 0, {
shadowColor: 'rgba(0, 0, 0, 0.5)' value: serverInfo.proxy_type_count.xtcp,
} name: 'XTCP'
} }
}] ],
}; itemStyle: {
myChart.setOption(option); emphasis: {
myChart.hideLoading() shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
myChart.setOption(option)
myChart.hideLoading()
} }
// 7 days // 7 days
function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) { function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) {
let params = { const params = {
width: '600px', width: '600px',
height: '400px' height: '400px'
} }
let myChart = echarts.init(document.getElementById(elementId), 'macarons', params); const myChart = echarts.init(document.getElementById(elementId), 'macarons', params)
myChart.showLoading() myChart.showLoading()
trafficInArr = trafficInArr.reverse() trafficInArr = trafficInArr.reverse()
trafficOutArr = trafficOutArr.reverse() trafficOutArr = trafficOutArr.reverse()
let now = new Date() let now = new Date()
now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6) now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6)
let dates = new Array() const dates = []
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate()) dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate())
now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
} }
let option = { const option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'shadow' type: 'shadow'
}, },
formatter: function(data) { formatter: function(data) {
let html = '' let html = ''
if (data.length > 0) { if (data.length > 0) {
html += data[0].name + '<br/>' html += data[0].name + '<br/>'
} }
for (let v of data) { for (const v of data) {
let colorEl = '<span style="display:inline-block;margin-right:5px;' + const colorEl =
'border-radius:10px;width:9px;height:9px;background-color:' + v.color + '"></span>'; '<span style="display:inline-block;margin-right:5px;' + 'border-radius:10px;width:9px;height:9px;background-color:' + v.color + '"></span>'
html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '<br/>' html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '<br/>'
} }
return html return html
} }
}, },
legend: { legend: {
data: ['Traffic In', 'Traffic Out'] data: ['Traffic In', 'Traffic Out']
}, },
grid: { grid: {
left: '3%', left: '3%',
right: '4%', right: '4%',
bottom: '3%', bottom: '3%',
containLabel: true containLabel: true
}, },
xAxis: [{ xAxis: [
type: 'category', {
data: dates type: 'category',
}], data: dates
yAxis: [{ }
type: 'value', ],
axisLabel: { yAxis: [
formatter: function(value) { {
return Humanize.fileSize(value) type: 'value',
} axisLabel: {
} formatter: function(value) {
}], return Humanize.fileSize(value)
series: [{ }
name: 'Traffic In', }
type: 'bar', }
data: trafficInArr ],
}, { series: [
{
name: 'Traffic Out', name: 'Traffic In',
type: 'bar', type: 'bar',
data: trafficOutArr data: trafficInArr
}] },
}; {
myChart.setOption(option); name: 'Traffic Out',
myChart.hideLoading() type: 'bar',
data: trafficOutArr
}
]
}
myChart.setOption(option)
myChart.hideLoading()
} }
export { export { DrawTrafficChart, DrawProxyChart, DrawProxyTrafficChart }
DrawTrafficChart,
DrawProxyChart,
DrawProxyTrafficChart
}

View File

@@ -0,0 +1,20 @@
import { Message } from 'element-ui'
export default function(api, init = {}) {
return new Promise(resolve => {
fetch(`/api/${api}`, Object.assign({ credentials: 'include' }, init))
.then(res => {
if (res.status < 200 || res.status >= 300) {
Message.warning('Get server info from frps failed!')
resolve()
return
}
resolve(res ? res.json() : undefined)
})
.catch(err => {
this.$message.error(err.message)
resolve()
})
})
}

View File

@@ -1,97 +1,97 @@
class BaseProxy { class BaseProxy {
constructor(proxyStats) { constructor(proxyStats) {
this.name = proxyStats.name this.name = proxyStats.name
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.encryption = proxyStats.conf.use_encryption this.encryption = proxyStats.conf.use_encryption
this.compression = proxyStats.conf.use_compression this.compression = proxyStats.conf.use_compression
} else { } else {
this.encryption = "" this.encryption = ''
this.compression = "" this.compression = ''
}
this.conns = proxyStats.cur_conns
this.traffic_in = proxyStats.today_traffic_in
this.traffic_out = proxyStats.today_traffic_out
this.last_start_time = proxyStats.last_start_time
this.last_close_time = proxyStats.last_close_time
this.status = proxyStats.status
} }
this.conns = proxyStats.cur_conns
this.traffic_in = proxyStats.today_traffic_in
this.traffic_out = proxyStats.today_traffic_out
this.last_start_time = proxyStats.last_start_time
this.last_close_time = proxyStats.last_close_time
this.status = proxyStats.status
}
} }
class TcpProxy extends BaseProxy { class TcpProxy extends BaseProxy {
constructor(proxyStats) { constructor(proxyStats) {
super(proxyStats) super(proxyStats)
this.type = "tcp" this.type = 'tcp'
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.addr = ":" + proxyStats.conf.remote_port this.addr = ':' + proxyStats.conf.remote_port
this.port = proxyStats.conf.remote_port this.port = proxyStats.conf.remote_port
} else { } else {
this.addr = "" this.addr = ''
this.port = "" this.port = ''
}
} }
}
} }
class UdpProxy extends BaseProxy { class UdpProxy extends BaseProxy {
constructor(proxyStats) { constructor(proxyStats) {
super(proxyStats) super(proxyStats)
this.type = "udp" this.type = 'udp'
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.addr = ":" + proxyStats.conf.remote_port this.addr = ':' + proxyStats.conf.remote_port
this.port = proxyStats.conf.remote_port this.port = proxyStats.conf.remote_port
} else { } else {
this.addr = "" this.addr = ''
this.port = "" this.port = ''
}
} }
}
} }
class HttpProxy extends BaseProxy { class HttpProxy extends BaseProxy {
constructor(proxyStats, port, subdomain_host) { constructor(proxyStats, port, subdomain_host) {
super(proxyStats) super(proxyStats)
this.type = "http" this.type = 'http'
this.port = port this.port = port
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.custom_domains = proxyStats.conf.custom_domains this.custom_domains = proxyStats.conf.custom_domains
this.host_header_rewrite = proxyStats.conf.host_header_rewrite this.host_header_rewrite = proxyStats.conf.host_header_rewrite
this.locations = proxyStats.conf.locations this.locations = proxyStats.conf.locations
if (proxyStats.conf.sub_domain != "") { if (proxyStats.conf.sub_domain !== '') {
this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host this.subdomain = proxyStats.conf.sub_domain + '.' + subdomain_host
} else { } else {
this.subdomain = "" this.subdomain = ''
} }
} else { } else {
this.custom_domains = "" this.custom_domains = ''
this.host_header_rewrite = "" this.host_header_rewrite = ''
this.subdomain = "" this.subdomain = ''
this.locations = "" this.locations = ''
}
} }
}
} }
class HttpsProxy extends BaseProxy { class HttpsProxy extends BaseProxy {
constructor(proxyStats, port, subdomain_host) { constructor(proxyStats, port, subdomain_host) {
super(proxyStats) super(proxyStats)
this.type = "https" this.type = 'https'
this.port = port this.port = port
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.custom_domains = proxyStats.conf.custom_domains this.custom_domains = proxyStats.conf.custom_domains
if (proxyStats.conf.sub_domain != "") { if (proxyStats.conf.sub_domain !== '') {
this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host this.subdomain = proxyStats.conf.sub_domain + '.' + subdomain_host
} else { } else {
this.subdomain = "" this.subdomain = ''
} }
} else { } else {
this.custom_domains = "" this.custom_domains = ''
this.subdomain = "" this.subdomain = ''
}
} }
}
} }
class StcpProxy extends BaseProxy { class StcpProxy extends BaseProxy {
constructor(proxyStats) { constructor(proxyStats) {
super(proxyStats) super(proxyStats)
this.type = "stcp" this.type = 'stcp'
} }
} }
export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy} export { BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy }

16
web/frps/vue.config.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
publicPath: './',
devServer: {
host: '127.0.0.1',
port: 8010,
proxy: {
'/api/': {
target: 'http://127.0.0.1:8080/api',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}

View File

@@ -1,107 +0,0 @@
const path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var url = require('url')
var publicPath = ''
module.exports = (options = {}) => ({
entry: {
vendor: './src/main'
},
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$/,
loader: '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'
}),
new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: false,
comments: false,
compress: {
warnings: false
}
}),
new VueLoaderPlugin()
],
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'
})

9478
web/frps/yarn.lock Normal file

File diff suppressed because it is too large Load Diff