17 Commits
1.0 ... 1.3

Author SHA1 Message Date
杨黄林
fe28fc5623 adjust color 2023-09-07 11:17:39 +08:00
杨黄林
8401948dc0 reset color;
add title bar;
2023-09-07 11:03:20 +08:00
杨黄林
4413eb3571 Update README_zh.md 2023-09-04 16:11:21 +08:00
杨黄林
fbadd44af5 Update README.md 2023-09-04 16:10:25 +08:00
杨黄林
e6488d3f1b update README 2023-09-04 16:08:19 +08:00
杨黄林
e4ecc57825 update README 2023-09-04 15:49:38 +08:00
杨黄林
c5e90c9e6c fix 'assets' dir not found exception;
fix add user fail issue
2023-09-04 15:44:28 +08:00
杨黄林
c8c6d17c9c update screenshots 2023-08-31 15:12:11 +08:00
杨黄林
dd87729455 fix chinese tips 2023-08-31 15:09:42 +08:00
杨黄林
e4d1caa4b9 update Makefile.cross-compiles 2023-08-30 23:57:59 +08:00
杨黄林
1fdc0fc63a add user and token format verify 2023-08-30 23:56:24 +08:00
杨黄林
d09fc2fe91 fix field verify issue 2023-08-30 23:15:50 +08:00
杨黄林
70915e6e0e change dir fp-multiuser to frps-multiuser 2023-08-30 23:15:20 +08:00
杨黄林
012a18ceb2 fix error tips with english 2023-08-30 19:55:32 +08:00
杨黄林
6ca0044dc3 add media query in layui-theme-dark.css 2023-08-30 16:34:26 +08:00
杨黄林
9096585a34 update README;update layui-theme-dark to v2.8.16 2023-08-30 16:32:54 +08:00
杨黄林
11855f0917 update README 2023-08-30 16:28:14 +08:00
20 changed files with 2667 additions and 2304 deletions

View File

@@ -1,8 +1,12 @@
export GO111MODULE=on export GO111MODULE=on
export CGO_ENABLED=0
export GOOS=linux
export GOARCH=amd64
build: frps-multiuser build: frps-multiuser
cp ./config/frps-multiuser.ini ./bin/frps-multiuser.ini cp ./config/frps-multiuser.ini ./bin/frps-multiuser.ini
cp -r ./assets/ ./bin/assets/ cp -r ./assets/ ./bin/assets/
frps-multiuser: frps-multiuser:
go build -o ./bin/frps-multiuser ./cmd/fp-multiuser rm -rf ./bin
go build -o ./bin/frps-multiuser ./cmd/frps-multiuser

View File

@@ -9,16 +9,16 @@ copy: build
cp -r ./assets/ ./release/assets/ cp -r ./assets/ ./release/assets/
build: build:
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-darwin-amd64 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-darwin-amd64 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-freebsd-386 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-freebsd-386 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-freebsd-amd64 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-freebsd-amd64 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-386 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-386 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-amd64 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-amd64 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-arm ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-arm ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-arm64 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-arm64 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-windows-386.exe ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-windows-386.exe ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-windows-amd64.exe ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-windows-amd64.exe ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips64 ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips64 ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips64le ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips64le ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips ./cmd/frps-multiuser
env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mipsle ./cmd/fp-multiuser env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mipsle ./cmd/frps-multiuser

View File

@@ -1,5 +1,7 @@
# frps-multiuser # frps-multiuser
[README](README.md) | [中文文档](README_zh.md)
frp server plugin to support multiple users for [frp](https://github.com/fatedier/frp). frp server plugin to support multiple users for [frp](https://github.com/fatedier/frp).
frps-multiuser will run as one single process and accept HTTP requests from frps. frps-multiuser will run as one single process and accept HTTP requests from frps.
@@ -9,7 +11,7 @@ frps-multiuser will run as one single process and accept HTTP requests from frps
![支持英文](screenshots/i18n.png) ![支持英文](screenshots/i18n.png)
![自动深色模式](screenshots/dark%20mode.png) ![自动深色模式](screenshots/dark%20mode.png)
## update notes ## Update Notes
+ **the default tokens file is frps-multiuser.ini now,ini file support comment** + **the default tokens file is frps-multiuser.ini now,ini file support comment**
+ **remove `-l`,it configure in `frps-multiuser.ini` now** + **remove `-l`,it configure in `frps-multiuser.ini` now**
@@ -21,13 +23,14 @@ frps-multiuser will run as one single process and accept HTTP requests from frps
+ **you can limit `ports`,`domains` and `subdomains` for each user now** + **you can limit `ports`,`domains` and `subdomains` for each user now**
***when a user is dynamic been `remove` or `disable`,it will take some time to be effective*** ***when a user is dynamic been `remove` or `disable`,it will take some time to be effective***
***the limit of `ports``domains``subdomains` only effective at `NewProxy`***
[README](README.md) | [中文文档](README_zh.md) ***the limit of `ports``domains``subdomains` only effective at `NewProxy`***
### Features ### Features
* Support multiple user authentication by tokens saved in file. * Support multiple user authentication by tokens saved in file.
* Support dynamic `add`,`remove`,`disable` or `enable` user
* Limit `ports`,`domains` and `subdomains` for each user
### Download ### Download
@@ -128,12 +131,41 @@ local_port = 22
remote_port = 6000 remote_port = 6000
``` ```
## Run as service
this example is for `ubuntu` and with `root` user
+ 1.unzip `frps-multiuser.zip` to dir `/root/frps-multiuser`
+ 2.touch a file with command `touch frps-multiuser.service` in dir `/root/frps-multiuser`.the file content is:
```ini
[Unit]
Description = frp multiuser service
After = network.target syslog.target
Wants = network.target
[Service]
Type = simple
# config of frps-multiuser.ini,you should change the file path
Environment=FRPS_MULTIUSER_OPTS="-c /root/frps-multiuser/frps-multiuser.ini"
# command of run frps-multiuser,you should change the file path
ExecStart = /root/frps-multiuser/frps-multiuser $FRPS_MULTIUSER_OPTS
[Install]
WantedBy = multi-user.target
```
+ 3.copy `frps-multiuser.service` to `/etc/systemd/system/` with command `cp /root/frps-multiuser.service /etc/systemd/system/`
+ 4.reload service with command `systemctl daemon-reload`
+ 5.start service with command `service frps-multiuser start`
## Issues & Ideas ## Issues & Ideas
___If you want visit mange ui from internet, you should change `plugin_addr` to `0.0.0.0`___
If you have any issues or ideas, put it on [issues](https://github.com/yhl452493373/frps-multiuser/issues). I will try my best to achieve it. If you have any issues or ideas, put it on [issues](https://github.com/yhl452493373/frps-multiuser/issues). I will try my best to achieve it.
## Credits ## Credits
+ [frp](https://github.com/fatedier/frp) + [frp](https://github.com/fatedier/frp)
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser) + [fp-multiuser](https://github.com/gofrp/fp-multiuser)
+ [layui](https://github.com/layui/layui) + [layui](https://github.com/layui/layui)
+ [layui-theme-dark](https://github.com/Sight-wcg/layui-theme-dark)

View File

@@ -23,11 +23,14 @@ frps-multiuser 会以一个单独的进程运行,并接收 frps 发送过来
+ **新增对用户的`端口`、`域名`、`二级域名`进行限制** + **新增对用户的`端口`、`域名`、`二级域名`进行限制**
***用户被`删除``禁用`后,不会马上生效,需要等一段时间*** ***用户被`删除``禁用`后,不会马上生效,需要等一段时间***
***用户`端口``域名``二级域名`限制仅在建立新连接(`NewProxy`)时生效*** ***用户`端口``域名``二级域名`限制仅在建立新连接(`NewProxy`)时生效***
### 功能 ### 功能
* 通过配置文件配置所有支持的用户名和 Token只允许匹配的 frpc 客户端登录。 * 通过配置文件配置所有支持的用户名和 Token只允许匹配的 frpc 客户端登录。
* 动态`添加``删除``禁用``启用`用户
* 对每个用户进行`端口``域名``二级域名`限制
### 下载 ### 下载
@@ -128,12 +131,41 @@ local_port = 22
remote_port = 6000 remote_port = 6000
``` ```
## 以服务的形式运行
本实例是在 `ubuntu` 下, 以 `root` 用户执操作
+ 1、解压 `frps-multiuser.zip` 到目录 `/root/frps-multiuser`
+ 2、在目录 `/root/frps-multiuser` 下 用命令创建文件:`touch frps-multiuser.service`。创建后修改文件内容:
```ini
[Unit]
Description = frp multiuser service
After = network.target syslog.target
Wants = network.target
[Service]
Type = simple
# 启动frps-multiuser的配置文件路径需修改为您的frps-multiuser.ini的路径
Environment=FRPS_MULTIUSER_OPTS="-c /root/frps-multiuser/frps-multiuser.ini"
# 启动frps-multiuser的命令需修改为您的frps-multiuser的安装路径
ExecStart = /root/frps-multiuser/frps-multiuser $FRPS_MULTIUSER_OPTS
[Install]
WantedBy = multi-user.target
```
+ 3、复制服务文件 `cp /root/frps-multiuser.service /etc/systemd/system/`
+ 4、重载服务 `systemctl daemon-reload`
+ 5、启动服务 `service frps-multiuser start`
## 使用 ## 使用
___如果要从外网访问管理界面, 需要把配置中的 `plugin_addr` 改为 `0.0.0.0`___
如果使用中有问题或者有其他想法,在[issues](https://github.com/yhl452493373/frps-multiuser/issues)上提出来。 如果我能搞定的话,我尽量搞。 如果使用中有问题或者有其他想法,在[issues](https://github.com/yhl452493373/frps-multiuser/issues)上提出来。 如果我能搞定的话,我尽量搞。
## 致谢 ## 致谢
+ [frp](https://github.com/fatedier/frp) + [frp](https://github.com/fatedier/frp)
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser) + [fp-multiuser](https://github.com/gofrp/fp-multiuser)
+ [layui](https://github.com/layui/layui) + [layui](https://github.com/layui/layui)
+ [layui-theme-dark](https://github.com/Sight-wcg/layui-theme-dark)

View File

@@ -1,5 +1,6 @@
{ {
"User Manage": "User Manage", "User Manage": "User Manage",
"frps multiuser": "frps multiuser",
"User": "User", "User": "User",
"Token": "Token", "Token": "Token",
"Notes": "Notes", "Notes": "Notes",
@@ -29,7 +30,8 @@
"Other error": "Other error", "Other error": "Other error",
"Param error": "Param error", "Param error": "Param error",
"User exist": "User exist", "User exist": "User exist",
"Token cannot be empty": "Token cannot be empty", "User format error": "User cannot be empty or include space char. It only allowed alphanumeric and underline.",
"Token format error": "Token cannot be empty or include space char. It allow include those special char: _!@#$%^&*()",
"Please check at least one user": "Please Check at least one user", "Please check at least one user": "Please Check at least one user",
"Operation confirm": "Operation confirm", "Operation confirm": "Operation confirm",
"Empty data": "Empty data", "Empty data": "Empty data",

View File

@@ -1,5 +1,6 @@
{ {
"User Manage": "用户管理", "User Manage": "用户管理",
"frps multiuser": "frps用户管理",
"User": "用户名(user)", "User": "用户名(user)",
"Token": "凭证(meta_token)", "Token": "凭证(meta_token)",
"Notes": "备注", "Notes": "备注",
@@ -29,7 +30,8 @@
"Other error": "其他异常", "Other error": "其他异常",
"Param error": "参数异常", "Param error": "参数异常",
"User exist": "用户已经存在", "User exist": "用户已经存在",
"Token cannot be empty": "Token 不能为空", "User format error": "用户不能为空或包含空格。只允许英文数字、字母、下划线",
"Token format error": "Token不能为空或包含空格。允许的特殊符号_!@#$%^&*()",
"Please check at least one user": "请选中需要操作的用户", "Please check at least one user": "请选中需要操作的用户",
"Operation confirm": "操作确认", "Operation confirm": "操作确认",
"Empty data": "无数据", "Empty data": "无数据",
@@ -38,7 +40,7 @@
"Allowed domains": "允许域名", "Allowed domains": "允许域名",
"Please input allowed domains": "请输入允许使用的域名,如:web01.domain.com,web02.domain.com", "Please input allowed domains": "请输入允许使用的域名,如:web01.domain.com,web02.domain.com",
"Allowed subdomains": "允许子域名", "Allowed subdomains": "允许子域名",
"Please input allowed subdomains": "请输入允许使用的端口,如:web01,web02", "Please input allowed subdomains": "请输入允许使用的子域名,如:web01,web02",
"Ports is invalid": "端口不正确", "Ports is invalid": "端口不正确",
"Domains is invalid": "域名不正确", "Domains is invalid": "域名不正确",
"Subdomains is invalid": "子域名不正确", "Subdomains is invalid": "子域名不正确",

View File

@@ -0,0 +1,128 @@
header {
background-color: #58b7ff;
color: #fff;
}
.layui-btn {
background-color: #409eff;
}
.layui-layer-btn .layui-layer-btn0 {
background-color: #409eff;
}
.layui-btn-primary {
background-color: transparent;
}
.layui-btn-primary:hover {
border-color: #79bbff;
}
.layui-input:focus,
.layui-textarea:focus {
border-color: #79bbff !important;
box-shadow: none;
}
.layui-form-danger + .layui-form-select .layui-input,
.layui-form-danger:focus {
border-color: #ff5722 !important;
}
.layui-laypage .layui-laypage-curr .layui-laypage-em {
background-color: #409eff;
}
.layui-laypage input:focus, .layui-laypage select:focus {
border-color: #79bbff !important;
box-shadow: none;
}
.layui-table-view .layui-table td[data-edit]:hover:after {
border-color: #79bbff;
}
.layui-form-checkbox[lay-skin=primary]:hover > i {
border-color: #79bbff;
}
.layui-form-checked[lay-skin=primary] > i {
background-color: #409eff;
border-color: #409eff !important;
}
.layui-table-checked {
background-color: #ecf5ff;
}
.layui-table-checked:hover {
background-color: #d9ecff;
}
.layui-table-cell-c:hover{
border-color: #79bbff;
}
@media (prefers-color-scheme: dark) {
header {
background-color: #395c74;
}
.layui-btn {
background-color: #4f80a1;
}
.layui-layer-btn .layui-layer-btn0 {
background-color: #4f80a1;
}
.layui-btn-primary {
background-color: transparent;
border-color: #484849;
}
.layui-btn-primary:hover {
border-color: #5f5f60;
}
.layui-input:focus,
.layui-textarea:focus {
border-color: #5f5f60 !important;
box-shadow: none;
}
.layui-laypage .layui-laypage-curr .layui-laypage-em {
background-color: #4f80a1;
}
.layui-laypage input:focus, .layui-laypage select:focus {
border-color: #5f5f60 !important;
box-shadow: none;
}
.layui-table-view .layui-table td[data-edit]:hover:after {
border-color: #5f5f60;
}
.layui-form-checkbox[lay-skin=primary]:hover > i {
border-color: #5f5f60;
}
.layui-form-checked[lay-skin=primary] > i {
background-color: #484849;
border-color: #484849 !important;
}
.layui-table-checked {
background-color: rgba(255, 255, 255, .04);
}
.layui-table-checked:hover {
background-color: rgba(255, 255, 255, .08);
}
.layui-table-cell-c:hover{
border-color: #5f5f60;
}
}

View File

@@ -1,8 +1,18 @@
body { html, body {
padding: 15px; padding: 0;
word-break: break-all; word-break: break-all;
} }
header .title {
padding: 10px 15px;
line-height: 40px;
font-size: 25px;
}
section {
padding: 15px 15px 0 15px;
}
#searchForm input { #searchForm input {
height: 30px; height: 30px;
line-height: 28px; line-height: 28px;
@@ -44,10 +54,14 @@ body {
line-height: 20px; line-height: 20px;
} }
.layui-btn-container{ .layui-btn-container {
height: 30px; height: 30px;
} }
.layui-layer-btn > a[class^=layui-layer-btn]{ .layui-layer-btn > a[class^=layui-layer-btn] {
line-height: 28px; line-height: 28px;
} }
.layui-table-page {
margin-bottom: 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,36 @@ $(function () {
Disable: 3 Disable: 3
} }
/**
* verify user value
* @param username
*/
function verifyUser(username) {
var valid = true;
if (username.trim() === '' || !/^\w+$/.test(username)) {
valid = false;
}
return {
valid: valid,
trim: username
};
}
/**
* verify token value
* @param token
*/
function verifyToken(token) {
var valid = true;
if (token.trim() === '' || !/^[\w!@#$%^&*()]+$/.test(token)) {
valid = false;
}
return {
valid: valid,
trim: token.trim()
};
}
/** /**
* verify comment is valid * verify comment is valid
* @param comment * @param comment
@@ -120,15 +150,43 @@ $(function () {
function langLoaded(lang) { function langLoaded(lang) {
//set verify rules //set verify rules
var verifyRules = { var verifyRules = {
user: function (value, item) {
var result = verifyUser(value);
if (!result.valid) {
return lang['UserFormatError'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
},
token: function (value, item) {
var result = verifyToken(value);
if (!result.valid) {
return lang['TokenFormatError'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
},
comment: function (value, item) { comment: function (value, item) {
var result = verifyComment(value); var result = verifyComment(value);
if (!result.valid) { if (!result.valid) {
return lang['CommentInvalid']; return lang['CommentInvalid'];
} }
if (typeof item === "function") { if (item != null) {
item && item(result.trim); if (typeof item === "function") {
} else { item && item(result.trim);
$(item).val(result.trim); } else {
$(item).val(result.trim);
}
} }
}, },
ports: function (value, item) { ports: function (value, item) {
@@ -136,10 +194,12 @@ $(function () {
if (!result.valid) { if (!result.valid) {
return lang['PortsInvalid']; return lang['PortsInvalid'];
} }
if (typeof item === "function") { if (item != null) {
item && item(result.trim); if (typeof item === "function") {
} else { item && item(result.trim);
$(item).val(result.trim); } else {
$(item).val(result.trim);
}
} }
}, },
domains: function (value, item) { domains: function (value, item) {
@@ -147,10 +207,12 @@ $(function () {
if (!result.valid) { if (!result.valid) {
return lang['DomainsInvalid']; return lang['DomainsInvalid'];
} }
if (typeof item === "function") { if (item != null) {
item && item(result.trim); if (typeof item === "function") {
} else { item && item(result.trim);
$(item).val(result.trim); } else {
$(item).val(result.trim);
}
} }
}, },
subdomains: function (value, item) { subdomains: function (value, item) {
@@ -158,14 +220,20 @@ $(function () {
if (!result.valid) { if (!result.valid) {
return lang['SubdomainsInvalid']; return lang['SubdomainsInvalid'];
} }
if (typeof item === "function") { if (item != null) {
item && item(result.trim); if (typeof item === "function") {
} else { item && item(result.trim);
$(item).val(result.trim); } else {
$(item).val(result.trim);
}
} }
} }
}; };
layui.form.verify(verifyRules);
layui.form.set({
verIncludeRequired: true,
verify: verifyRules
});
layui.table.render({ layui.table.render({
elem: '#tokenTable', elem: '#tokenTable',
@@ -217,8 +285,11 @@ $(function () {
var after = $.extend(true, {}, obj.data); var after = $.extend(true, {}, obj.data);
var verifyMsg = false; var verifyMsg = false;
if (field === 'token') { if (field === 'token') {
if (value.trim() === '') { verifyMsg = verifyRules.token(value, function (trim) {
layui.layer.msg(lang['TokenEmpty']) updateTableField(obj, field, trim)
});
if (verifyMsg) {
layui.layer.msg(verifyMsg);
return obj.reedit(); return obj.reedit();
} }
@@ -318,7 +389,7 @@ $(function () {
btn: [lang['Confirm'], lang['Cancel']], btn: [lang['Confirm'], lang['Cancel']],
btn1: function (index) { btn1: function (index) {
if (layui.form.validate('#addUserForm')) { if (layui.form.validate('#addUserForm')) {
add(layui.form.val('addUserForm'), index) add(layui.form.val('addUserForm'), index);
} }
}, },
btn2: function (index) { btn2: function (index) {
@@ -540,6 +611,12 @@ $(function () {
reason = lang['ParamError']; reason = lang['ParamError'];
else if (result.code === 2) else if (result.code === 2)
reason = lang['UserExist']; reason = lang['UserExist'];
else if (result.code === 3)
reason = lang['ParamError'];
else if (result.code === 4)
reason = lang['UserFormatError'];
else if (result.code === 5)
reason = lang['TokenFormatError'];
layui.layer.msg(lang['OperateFailed'] + ',' + reason) layui.layer.msg(lang['OperateFailed'] + ',' + reason)
} }

View File

@@ -5,6 +5,7 @@
<link rel="stylesheet" href="./static/layui/css/layui.css"> <link rel="stylesheet" href="./static/layui/css/layui.css">
<link rel="stylesheet" href="./static/css/layui-theme-dark.css"> <link rel="stylesheet" href="./static/css/layui-theme-dark.css">
<link rel="stylesheet" href="./static/css/index.css"> <link rel="stylesheet" href="./static/css/index.css">
<link rel="stylesheet" href="./static/css/index-color.css">
<script src="./static/layui/layui.js"></script> <script src="./static/layui/layui.js"></script>
<script src="./static/js/index.js"></script> <script src="./static/js/index.js"></script>
<style> <style>
@@ -18,42 +19,48 @@
</style> </style>
</head> </head>
<body> <body>
<form class="layui-form layui-row layui-col-space16" id="searchForm" lay-filter="searchForm"> <header>
<div class="layui-col-md3"> <div class="title">${ .FrpsMultiuser }</div>
<div class="layui-input-wrap"> </header>
<div class="layui-input-prefix"> <section>
<i class="layui-icon layui-icon-username"></i> <form class="layui-form layui-row layui-col-space16" id="searchForm" lay-filter="searchForm">
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="user" placeholder="${ .User }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
<input type="text" name="user" value="" placeholder="${ .User }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
</div> <div class="layui-col-md3">
<div class="layui-col-md3"> <div class="layui-input-wrap">
<div class="layui-input-wrap"> <div class="layui-input-prefix">
<div class="layui-input-prefix"> <i class="layui-icon layui-icon-vercode"></i>
<i class="layui-icon layui-icon-vercode"></i> </div>
<input type="text" name="token" placeholder="${ .Token }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
<input type="text" name="token" placeholder="${ .Token }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
</div> <div class="layui-col-md3">
<div class="layui-col-md3"> <div class="layui-input-wrap">
<div class="layui-input-wrap"> <div class="layui-input-prefix">
<div class="layui-input-prefix"> <i class="layui-icon layui-icon-note"></i>
<i class="layui-icon layui-icon-note"></i> </div>
<input type="text" name="comment" placeholder="${ .Notes }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
<input type="text" name="comment" placeholder="${ .Notes }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div> </div>
</div> <div class="layui-col-md3">
<div class="layui-col-md3"> <div class="layui-btn-container">
<div class="layui-btn-container"> <button class="layui-btn layui-btn-sm" id="searchBtn">${ .Search }</button>
<button class="layui-btn layui-btn-sm" id="searchBtn">${ .Search }</button> <button class="layui-btn layui-btn-sm layui-btn-primary" type="reset" id="resetBtn">${ .Reset }</button>
<button class="layui-btn layui-btn-sm layui-btn-primary" type="reset" id="resetBtn">${ .Reset }</button> </div>
</div> </div>
</div> </form>
</form> <table id="tokenTable" lay-filter="tokenTable"></table>
<table id="tokenTable" lay-filter="tokenTable"></table> </section>
<script type="text/html" id="toolbarTemplate"> <script type="text/html" id="toolbarTemplate">
<div class="layui-btn-container"> <div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add">${ .NewUser }</button> <button class="layui-btn layui-btn-sm" lay-event="add">${ .NewUser }</button>
@@ -73,21 +80,19 @@
</div> </div>
</script> </script>
<script type="text/html" id="addTemplate"> <script type="text/html" id="addTemplate">
<div class="layui-form" id="addUserForm" lay-filter="addUserForm"> <form class="layui-form" id="addUserForm" lay-filter="addUserForm">
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">${ .User }</label> <label class="layui-form-label">${ .User }</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="text" name="user" placeholder="${ .PleaseInputUserAccount }" autocomplete="off" <input type="text" name="user" lay-verify="user" placeholder="${ .PleaseInputUserAccount }"
class="layui-input" autocomplete="off" class="layui-input"/>
lay-verify="required">
</div> </div>
</div> </div>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">${ .Token }</label> <label class="layui-form-label">${ .Token }</label>
<div class="layui-input-block"> <div class="layui-input-block">
<input type="text" name="token" lay-verify="required" placeholder="${ .PleaseInputUserToken }" <input type="text" name="token" lay-verify="token" placeholder="${ .PleaseInputUserToken }"
class="layui-input" autocomplete="off" class="layui-input"/>
autocomplete="off">
</div> </div>
</div> </div>
<div class="layui-form-item layui-form-text"> <div class="layui-form-item layui-form-text">
@@ -118,7 +123,7 @@
autocomplete="off" class="layui-textarea"></textarea> autocomplete="off" class="layui-textarea"></textarea>
</div> </div>
</div> </div>
</div> </form>
</script> </script>
</body> </body>
</html> </html>

View File

@@ -9,6 +9,7 @@ import (
"io/fs" "io/fs"
"log" "log"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
) )
@@ -33,20 +34,29 @@ var rootCmd = &cobra.Command{
log.Println(version) log.Println(version)
return nil return nil
} }
executable, err := os.Executable()
if err != nil {
log.Printf("error get program path: %v", err)
return err
}
rootDir := filepath.Dir(executable)
common, tokens, ports, domains, subdomains, iniFile, err := ParseConfigFile(configFile) common, tokens, ports, domains, subdomains, iniFile, err := ParseConfigFile(configFile)
if err != nil { if err != nil {
log.Printf("fail to start frps-multiuser : %v", err) log.Printf("fail to start frps-multiuser : %v", err)
return nil return err
} }
s, err := server.New(controller.HandleController{ s, err := server.New(
CommonInfo: common, rootDir,
Tokens: tokens, controller.HandleController{
Ports: ports, CommonInfo: common,
Domains: domains, Tokens: tokens,
Subdomains: subdomains, Ports: ports,
ConfigFile: configFile, Domains: domains,
IniFile: iniFile, Subdomains: subdomains,
}) ConfigFile: configFile,
IniFile: iniFile,
})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -15,12 +15,16 @@ import (
) )
const ( const (
Success = 0 Success = 0
ParamError = 1 ParamError = 1
UserExist = 2 UserExist = 2
SaveError = 3 SaveError = 3
UserFormatError = 4
TokenFormatError = 5
) )
var UserFormatReg = regexp.MustCompile("^\\w+$")
var TokenFormatReg = regexp.MustCompile("^[\\w!@#$%^&*()]+$")
var TrimAllSpaceReg = regexp.MustCompile("[\\n\\t\\r\\s]") var TrimAllSpaceReg = regexp.MustCompile("[\\n\\t\\r\\s]")
var TrimBreakLineReg = regexp.MustCompile("[\\n\\t\\r]") var TrimBreakLineReg = regexp.MustCompile("[\\n\\t\\r]")
@@ -160,6 +164,7 @@ func (c *HandleController) MakeManagerFunc() func(context *gin.Context) {
return func(context *gin.Context) { return func(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{ context.HTML(http.StatusOK, "index.html", gin.H{
"UserManage": ginI18n.MustGetMessage(context, "User Manage"), "UserManage": ginI18n.MustGetMessage(context, "User Manage"),
"FrpsMultiuser": ginI18n.MustGetMessage(context, "frps multiuser"),
"User": ginI18n.MustGetMessage(context, "User"), "User": ginI18n.MustGetMessage(context, "User"),
"Token": ginI18n.MustGetMessage(context, "Token"), "Token": ginI18n.MustGetMessage(context, "Token"),
"Notes": ginI18n.MustGetMessage(context, "Notes"), "Notes": ginI18n.MustGetMessage(context, "Notes"),
@@ -210,7 +215,8 @@ func (c *HandleController) MakeLangFunc() func(context *gin.Context) {
"OperateError": ginI18n.MustGetMessage(context, "Operate error"), "OperateError": ginI18n.MustGetMessage(context, "Operate error"),
"OperateFailed": ginI18n.MustGetMessage(context, "Operate failed"), "OperateFailed": ginI18n.MustGetMessage(context, "Operate failed"),
"UserExist": ginI18n.MustGetMessage(context, "User exist"), "UserExist": ginI18n.MustGetMessage(context, "User exist"),
"TokenEmpty": ginI18n.MustGetMessage(context, "Token cannot be empty"), "UserFormatError": ginI18n.MustGetMessage(context, "User format error"),
"TokenFormatError": ginI18n.MustGetMessage(context, "Token format error"),
"ShouldCheckUser": ginI18n.MustGetMessage(context, "Please check at least one user"), "ShouldCheckUser": ginI18n.MustGetMessage(context, "Please check at least one user"),
"OperationConfirm": ginI18n.MustGetMessage(context, "Operation confirm"), "OperationConfirm": ginI18n.MustGetMessage(context, "Operation confirm"),
"EmptyData": ginI18n.MustGetMessage(context, "Empty data"), "EmptyData": ginI18n.MustGetMessage(context, "Empty data"),
@@ -221,6 +227,7 @@ func (c *HandleController) MakeLangFunc() func(context *gin.Context) {
"DomainsInvalid": ginI18n.MustGetMessage(context, "Domains is invalid"), "DomainsInvalid": ginI18n.MustGetMessage(context, "Domains is invalid"),
"SubdomainsInvalid": ginI18n.MustGetMessage(context, "Subdomains is invalid"), "SubdomainsInvalid": ginI18n.MustGetMessage(context, "Subdomains is invalid"),
"CommentInvalid": ginI18n.MustGetMessage(context, "Comment is invalid"), "CommentInvalid": ginI18n.MustGetMessage(context, "Comment is invalid"),
"ParamError": ginI18n.MustGetMessage(context, "Param error"),
}) })
} }
} }
@@ -313,6 +320,14 @@ func (c *HandleController) MakeAddTokenFunc() func(context *gin.Context) {
context.JSON(http.StatusOK, &response) context.JSON(http.StatusOK, &response)
return return
} }
if !UserFormatReg.MatchString(info.User) {
log.Printf("user add failed, user format error")
response.Success = false
response.Code = UserFormatError
response.Message = fmt.Sprintf("user add failed, user format error")
context.JSON(http.StatusOK, &response)
return
}
if _, exist := c.Tokens[info.User]; exist { if _, exist := c.Tokens[info.User]; exist {
log.Printf("user add failed, user [%v] exist", info.User) log.Printf("user add failed, user [%v] exist", info.User)
response.Success = false response.Success = false
@@ -321,6 +336,16 @@ func (c *HandleController) MakeAddTokenFunc() func(context *gin.Context) {
context.JSON(http.StatusOK, &response) context.JSON(http.StatusOK, &response)
return return
} }
if !TokenFormatReg.MatchString(info.Token) {
log.Printf("user add failed, token format error")
response.Success = false
response.Code = TokenFormatError
response.Message = fmt.Sprintf("user add failed, token format error")
context.JSON(http.StatusOK, &response)
return
}
replaceSpaceToken := TrimAllSpaceReg.ReplaceAllString(info.Token, "")
info.Token = replaceSpaceToken
c.Tokens[info.User] = info c.Tokens[info.User] = info
usersSection, _ := c.IniFile.GetSection("users") usersSection, _ := c.IniFile.GetSection("users")
@@ -388,7 +413,18 @@ func (c *HandleController) MakeUpdateTokensFunc() func(context *gin.Context) {
comment := TrimBreakLineReg.ReplaceAllString(after.Comment, "") comment := TrimBreakLineReg.ReplaceAllString(after.Comment, "")
after.Comment = comment after.Comment = comment
key.Comment = comment key.Comment = comment
key.SetValue(after.Token)
if !TokenFormatReg.MatchString(after.Token) {
log.Printf("update failed, token format error")
response.Success = false
response.Code = TokenFormatError
response.Message = "user update failed, token format error "
context.JSON(http.StatusOK, &response)
return
}
replaceSpaceToken := TrimAllSpaceReg.ReplaceAllString(after.Token, "")
after.Token = replaceSpaceToken
key.SetValue(replaceSpaceToken)
if before.Ports != after.Ports { if before.Ports != after.Ports {
portsSection, _ := c.IniFile.GetSection("ports") portsSection, _ := c.IniFile.GetSection("ports")

View File

@@ -6,6 +6,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"log" "log"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
) )
@@ -24,9 +26,15 @@ func NewHandleController(config *HandleController) *HandleController {
return config return config
} }
func (c *HandleController) Register(engine *gin.Engine) { func (c *HandleController) Register(rootDir string, engine *gin.Engine) {
assets := filepath.Join(rootDir, "assets")
_, err := os.Stat(assets)
if err != nil && !os.IsExist(err) {
assets = "./assets"
}
engine.Delims("${", "}") engine.Delims("${", "}")
engine.LoadHTMLGlob("./assets/templates/*") engine.LoadHTMLGlob(filepath.Join(assets, "templates/*"))
engine.POST("/handler", c.MakeHandlerFunc()) engine.POST("/handler", c.MakeHandlerFunc())
var group *gin.RouterGroup var group *gin.RouterGroup
@@ -37,7 +45,7 @@ func (c *HandleController) Register(engine *gin.Engine) {
} else { } else {
group = engine.Group("/") group = engine.Group("/")
} }
group.Static("/static", "./assets/static") group.Static("/static", filepath.Join(assets, "static"))
group.GET("/", c.MakeManagerFunc()) group.GET("/", c.MakeManagerFunc())
group.GET("/lang", c.MakeLangFunc()) group.GET("/lang", c.MakeLangFunc())
group.GET("/tokens", c.MakeQueryTokensFunc()) group.GET("/tokens", c.MakeQueryTokensFunc())

View File

@@ -13,21 +13,24 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
type Server struct { type Server struct {
cfg controller.HandleController cfg controller.HandleController
s *http.Server s *http.Server
done chan struct{} done chan struct{}
rootDir string
} }
func New(cfg controller.HandleController) (*Server, error) { func New(rootDir string, cfg controller.HandleController) (*Server, error) {
s := &Server{ s := &Server{
cfg: cfg, cfg: cfg,
done: make(chan struct{}), done: make(chan struct{}),
rootDir: rootDir,
} }
if err := s.init(); err != nil { if err := s.init(); err != nil {
return nil, err return nil, err
@@ -74,14 +77,10 @@ func LoadSupportLanguage(dir string) ([]language.Tag, error) {
var tags []language.Tag var tags []language.Tag
files, err := os.Open(dir) files, err := os.Open(dir)
if err != nil {
log.Printf("error opening directory: %v", err)
return tags, err
}
fileList, err := files.Readdir(-1) fileList, err := files.Readdir(-1)
if err != nil { if err != nil {
log.Printf("error reading directory: %v", err) log.Printf("error read lang directory: %v", err)
return tags, err return tags, err
} }
@@ -103,15 +102,21 @@ func LoadSupportLanguage(dir string) ([]language.Tag, error) {
return tags, nil return tags, nil
} }
func GinI18nLocalize() gin.HandlerFunc { func GinI18nLocalize(rootDir string) gin.HandlerFunc {
dir := "./assets/lang" assets := filepath.Join(rootDir, "assets")
tags, err := LoadSupportLanguage(dir) _, err := os.Stat(assets)
if err != nil && !os.IsExist(err) {
assets = "./assets"
}
lang := filepath.Join(assets, "lang")
tags, err := LoadSupportLanguage(lang)
if err != nil { if err != nil {
log.Panicf("language file is not found: %v", err) log.Panicf("language file is not found: %v", err)
} }
return ginI18n.Localize( return ginI18n.Localize(
ginI18n.WithBundle(&ginI18n.BundleCfg{ ginI18n.WithBundle(&ginI18n.BundleCfg{
RootPath: dir, RootPath: lang,
AcceptLanguage: tags, AcceptLanguage: tags,
DefaultLanguage: language.Chinese, DefaultLanguage: language.Chinese,
FormatBundleFile: "json", FormatBundleFile: "json",
@@ -133,10 +138,10 @@ func GinI18nLocalize() gin.HandlerFunc {
func (s *Server) initHTTPServer() error { func (s *Server) initHTTPServer() error {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
engine := gin.New() engine := gin.New()
engine.Use(GinI18nLocalize()) engine.Use(GinI18nLocalize(s.rootDir))
s.s = &http.Server{ s.s = &http.Server{
Handler: engine, Handler: engine,
} }
controller.NewHandleController(&s.cfg).Register(engine) controller.NewHandleController(&s.cfg).Register(s.rootDir, engine)
return nil return nil
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 134 KiB