8 Commits
1.0 ... 1.1

Author SHA1 Message Date
杨黄林
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
12 changed files with 2364 additions and 2238 deletions

View File

@@ -5,4 +5,4 @@ build: frps-multiuser
cp -r ./assets/ ./bin/assets/
frps-multiuser:
go build -o ./bin/frps-multiuser ./cmd/fp-multiuser
go build -o ./bin/frps-multiuser ./cmd/frps-multiuser

View File

@@ -9,16 +9,16 @@ copy: build
cp -r ./assets/ ./release/assets/
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=freebsd GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-freebsd-386 ./cmd/fp-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=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-386 ./cmd/fp-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=arm go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-arm ./cmd/fp-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=windows GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-windows-386.exe ./cmd/fp-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=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips64 ./cmd/fp-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=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./release/frps-multiuser-linux-mips ./cmd/fp-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=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/frps-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/frps-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/frps-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/frps-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/frps-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/frps-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

@@ -28,6 +28,8 @@ frps-multiuser will run as one single process and accept HTTP requests from frps
### Features
* 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
@@ -137,3 +139,4 @@ If you have any issues or ideas, put it on [issues](https://github.com/yhl452493
+ [frp](https://github.com/fatedier/frp)
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser)
+ [layui](https://github.com/layui/layui)
+ [layui-theme-dark](https://github.com/Sight-wcg/layui-theme-dark)

View File

@@ -28,6 +28,8 @@ frps-multiuser 会以一个单独的进程运行,并接收 frps 发送过来
### 功能
* 通过配置文件配置所有支持的用户名和 Token只允许匹配的 frpc 客户端登录。
* 动态`添加``删除``禁用``启用`用户
* 对每个用户进行`端口``域名``二级域名`限制
### 下载
@@ -137,3 +139,4 @@ remote_port = 6000
+ [frp](https://github.com/fatedier/frp)
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser)
+ [layui](https://github.com/layui/layui)
+ [layui-theme-dark](https://github.com/Sight-wcg/layui-theme-dark)

View File

@@ -29,7 +29,8 @@
"Other error": "Other error",
"Param error": "Param error",
"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",
"Operation confirm": "Operation confirm",
"Empty data": "Empty data",

View File

@@ -29,7 +29,8 @@
"Other error": "其他异常",
"Param error": "参数异常",
"User exist": "用户已经存在",
"Token cannot be empty": "Token 不能为空",
"User format error": "用户不能为空或包含空格。只允许英文数字、字母、下划线",
"Token format error": "Token不能为空或包含空格。允许的特殊符号_!@#$%^&*()",
"Please check at least one user": "请选中需要操作的用户",
"Operation confirm": "操作确认",
"Empty data": "无数据",

View File

@@ -1,4 +1,3 @@
/* https://github.com/Sight-wcg/layui-theme-dark */
@media (prefers-color-scheme: dark) {
:root {
/* =====色板===== */
@@ -2048,28 +2047,37 @@
background-color: var(--lay-color-bg-3)
}
.layui-layer-imgnext, .layui-layer-imgprev {
.layui-layer-photos {
background: none;
box-shadow: none;
}
.layui-layer-photos-prev, .layui-layer-photos-next {
color: var(--lay-color-gray-9)
}
.layui-layer-imgnext:hover, .layui-layer-imgprev:hover {
color: var(--lay-color-gray-8)
.layui-layer-photos-prev:hover, .layui-layer-photos-next:hover {
color: var(--lay-color-text-1)
}
.layui-layer-imgbar {
.layui-layer-photos-toolbar {
background-color: #333 \9;
background-color: var(--lay-color-bg-5);
color: var(--lay-color-text-1)
}
.layui-layer-imgtit * {
.layui-layer-photos-toolbar * {
color: var(--lay-color-text-1)
}
.layui-layer-imgtit a:hover {
.layui-layer-photos-toolbar a:hover {
color: var(--lay-color-text-2)
}
.layui-layer-photos-header > span:hover {
background-color: var(--lay-color-fill-2)
}
.layui-layer-tips i.layui-layer-TipsB, .layui-layer-tips i.layui-layer-TipsT {
border-right-color: var(--lay-color-bg-5)
}

View File

@@ -6,6 +6,36 @@ $(function () {
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
* @param comment
@@ -120,52 +150,90 @@ $(function () {
function langLoaded(lang) {
//set verify rules
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) {
var result = verifyComment(value);
if (!result.valid) {
return lang['CommentInvalid'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
},
ports: function (value, item) {
var result = verifyPorts(value);
if (!result.valid) {
return lang['PortsInvalid'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
},
domains: function (value, item) {
var result = verifyDomains(value);
if (!result.valid) {
return lang['DomainsInvalid'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
},
subdomains: function (value, item) {
var result = verifySubdomains(value);
if (!result.valid) {
return lang['SubdomainsInvalid'];
}
if (item != null) {
if (typeof item === "function") {
item && item(result.trim);
} else {
$(item).val(result.trim);
}
}
}
};
layui.form.verify(verifyRules);
layui.form.set({
verIncludeRequired: true,
verify: verifyRules
});
layui.table.render({
elem: '#tokenTable',
@@ -217,8 +285,11 @@ $(function () {
var after = $.extend(true, {}, obj.data);
var verifyMsg = false;
if (field === 'token') {
if (value.trim() === '') {
layui.layer.msg(lang['TokenEmpty'])
verifyMsg = verifyRules.token(value, function (trim) {
updateTableField(obj, field, trim)
});
if (verifyMsg) {
layui.layer.msg(verifyMsg);
return obj.reedit();
}
@@ -318,7 +389,7 @@ $(function () {
btn: [lang['Confirm'], lang['Cancel']],
btn1: function (index) {
if (layui.form.validate('#addUserForm')) {
add(layui.form.val('addUserForm'), index)
add(layui.form.val('addUserForm'), index);
}
},
btn2: function (index) {
@@ -540,6 +611,12 @@ $(function () {
reason = lang['ParamError'];
else if (result.code === 2)
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)
}

View File

@@ -24,7 +24,7 @@
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="user" value="" placeholder="${ .User }" class="layui-input" autocomplete="off"
<input type="text" name="user" placeholder="${ .User }" class="layui-input" autocomplete="off"
lay-affix="clear">
</div>
</div>
@@ -73,21 +73,19 @@
</div>
</script>
<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">
<label class="layui-form-label">${ .User }</label>
<div class="layui-input-block">
<input type="text" name="user" placeholder="${ .PleaseInputUserAccount }" autocomplete="off"
class="layui-input"
lay-verify="required">
<input type="text" name="user" lay-verify="user" placeholder="${ .PleaseInputUserAccount }"
autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">${ .Token }</label>
<div class="layui-input-block">
<input type="text" name="token" lay-verify="required" placeholder="${ .PleaseInputUserToken }"
class="layui-input"
autocomplete="off">
<input type="text" name="token" lay-verify="token" placeholder="${ .PleaseInputUserToken }"
autocomplete="off" class="layui-input"/>
</div>
</div>
<div class="layui-form-item layui-form-text">
@@ -118,7 +116,7 @@
autocomplete="off" class="layui-textarea"></textarea>
</div>
</div>
</div>
</form>
</script>
</body>
</html>

View File

@@ -19,8 +19,12 @@ const (
ParamError = 1
UserExist = 2
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 TrimBreakLineReg = regexp.MustCompile("[\\n\\t\\r]")
@@ -210,7 +214,8 @@ func (c *HandleController) MakeLangFunc() func(context *gin.Context) {
"OperateError": ginI18n.MustGetMessage(context, "Operate error"),
"OperateFailed": ginI18n.MustGetMessage(context, "Operate failed"),
"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"),
"OperationConfirm": ginI18n.MustGetMessage(context, "Operation confirm"),
"EmptyData": ginI18n.MustGetMessage(context, "Empty data"),
@@ -221,6 +226,7 @@ func (c *HandleController) MakeLangFunc() func(context *gin.Context) {
"DomainsInvalid": ginI18n.MustGetMessage(context, "Domains is invalid"),
"SubdomainsInvalid": ginI18n.MustGetMessage(context, "Subdomains is invalid"),
"CommentInvalid": ginI18n.MustGetMessage(context, "Comment is invalid"),
"ParamError": ginI18n.MustGetMessage(context, "Param error"),
})
}
}
@@ -313,6 +319,14 @@ func (c *HandleController) MakeAddTokenFunc() func(context *gin.Context) {
context.JSON(http.StatusOK, &response)
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 {
log.Printf("user add failed, user [%v] exist", info.User)
response.Success = false
@@ -321,6 +335,16 @@ func (c *HandleController) MakeAddTokenFunc() func(context *gin.Context) {
context.JSON(http.StatusOK, &response)
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
usersSection, _ := c.IniFile.GetSection("users")
@@ -388,7 +412,18 @@ func (c *HandleController) MakeUpdateTokensFunc() func(context *gin.Context) {
comment := TrimBreakLineReg.ReplaceAllString(after.Comment, "")
after.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 {
portsSection, _ := c.IniFile.GetSection("ports")