mirror of
https://github.com/yhl452493373/frps-panel.git
synced 2026-04-04 06:16:59 +08:00
the first commit, finish almost all function what i need
This commit is contained in:
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
8
Makefile
Normal file
8
Makefile
Normal file
@@ -0,0 +1,8 @@
|
||||
export GO111MODULE=on
|
||||
|
||||
build: frps-multiuser
|
||||
cp ./config/frps-multiuser.ini ./bin/frps-multiuser.ini
|
||||
cp -r ./assets/ ./bin/assets/
|
||||
|
||||
frps-multiuser:
|
||||
go build -o ./bin/frps-multiuser ./cmd/fp-multiuser
|
||||
24
Makefile.cross-compiles
Normal file
24
Makefile.cross-compiles
Normal file
@@ -0,0 +1,24 @@
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
|
||||
package: copy
|
||||
sh ./package.sh
|
||||
|
||||
copy: build
|
||||
cp ./config/frps-multiuser.ini ./release/frps-multiuser.ini
|
||||
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
|
||||
122
README.md
Normal file
122
README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# frps-multiuser
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## update notes
|
||||
|
||||
+ **the default tokens file is frps-multiuser.ini now,ini file support comment**
|
||||
+ **remove `-l`,it configure in `frps-multiuser.ini` now**
|
||||
+ **change `-f` to `-c`,the same as `frps`**
|
||||
+ **if \[users\] section is empty,the authentication will only be handle by frps**
|
||||
+ **if user under \[disabled\] section ,and the value is `disable`, it means that user is be disabled, and can not connect to server**
|
||||
+ **add a manage ui, and change color mode base on browser**
|
||||
+ **you can dynamic `add`,`remove`,`disable` or `enable` 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***
|
||||
***the limit of `ports`、`domains`、`subdomains` only effective at `NewProxy`***
|
||||
|
||||
[README](README.md) | [中文文档](README_zh.md)
|
||||
|
||||
### Features
|
||||
|
||||
* Support multiple user authentication by tokens saved in file.
|
||||
|
||||
### Download
|
||||
|
||||
Download frps-multiuser binary file from [Release](../../releases).
|
||||
|
||||
### Requirements
|
||||
|
||||
frp version >= v0.31.0
|
||||
|
||||
### Usage
|
||||
|
||||
1. Create file `frps-multiuser.ini` including all support usernames and tokens.
|
||||
|
||||
```ini
|
||||
[common]
|
||||
;plugin listen ip
|
||||
plugin_addr = 127.0.0.1
|
||||
;plugin listen port
|
||||
plugin_port = 7200
|
||||
;user and passwd for basic auth protect
|
||||
admin_user = admin
|
||||
admin_pwd = admin
|
||||
|
||||
[users]
|
||||
;user1
|
||||
user1 = 123
|
||||
;user2
|
||||
user2 = abc
|
||||
|
||||
[disabled]
|
||||
;user2 is disable
|
||||
user2 = disable
|
||||
```
|
||||
|
||||
One user each line. Username and token are split by `=`.
|
||||
|
||||
2. Run frps-multiuser:
|
||||
|
||||
`./frps-multiuser -c ./frps-multiuser.ini`
|
||||
|
||||
3. Register plugin in frps.
|
||||
|
||||
```ini
|
||||
# frps.ini
|
||||
[common]
|
||||
bind_port = 7000
|
||||
|
||||
[plugin.multiuser]
|
||||
addr = 127.0.0.1:7200
|
||||
path = /handler
|
||||
ops = Login,NewWorkConn,NewUserConn,NewProxy,Ping
|
||||
```
|
||||
|
||||
4. Specify username and meta_token in frpc configure file.
|
||||
|
||||
For user1:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
user = user1
|
||||
meta_token = 123
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
```
|
||||
|
||||
For user2:(user2 can't connect to server,because it is disable)
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
user = user2
|
||||
meta_token = abc
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
```
|
||||
|
||||
# Credits
|
||||
|
||||
+ [frp](https://github.com/fatedier/frp)
|
||||
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser)
|
||||
+ [layui](https://github.com/layui/layui)
|
||||
123
README_zh.md
Normal file
123
README_zh.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# frps-multiuser
|
||||
|
||||
[README](README.md) | [中文文档](README_zh.md)
|
||||
|
||||
frps-multiuser 是 [frp](https://github.com/fatedier/frp) 的一个服务端插件,用于支持多用户鉴权。
|
||||
|
||||
frps-multiuser 会以一个单独的进程运行,并接收 frps 发送过来的 HTTP 请求。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 更新说明
|
||||
|
||||
+ **配置文件改为ini格式,便于增加注释**
|
||||
+ **删除-l参数,其需要的配置由`frps-multiuser.ini`决定**
|
||||
+ **指定配置文件的参数由`-f`改为`-c`,和`frps`一致**
|
||||
+ **配置文件中,\[users\]节下如无用户信息,则直接由frps的token认证**
|
||||
+ **配置文件中,\[disabled\]节下用户名对应的值如果为`disable`,则说明该账户被禁用,无法连接到服务器**
|
||||
+ **增加了管理界面,并且会根据浏览器主题自动切换深色或浅色模式**
|
||||
+ **新增动态`添加`、`删除`、`禁用`、`启用`用户**
|
||||
+ **新增对用户的`端口`、`域名`、`二级域名`进行限制**
|
||||
|
||||
***用户被`删除`或`禁用`后,不会马上生效,需要等一段时间***
|
||||
***用户`端口`、`域名`、`二级域名`限制仅在建立新连接(`NewProxy`)时生效***
|
||||
|
||||
### 功能
|
||||
|
||||
* 通过配置文件配置所有支持的用户名和 Token,只允许匹配的 frpc 客户端登录。
|
||||
|
||||
### 下载
|
||||
|
||||
通过 [Release](../../releases) 页面下载对应系统版本的二进制文件到本地。
|
||||
|
||||
### 要求
|
||||
|
||||
需要 frp 版本 >= v0.31.0
|
||||
|
||||
### 使用示例
|
||||
|
||||
1. 创建 `frps-multiuser.ini` 文件,内容为所有支持的用户名和 token。
|
||||
|
||||
```ini
|
||||
[common]
|
||||
;插件监听地址
|
||||
plugin_addr = 127.0.0.1
|
||||
;插件端口
|
||||
plugin_port = 7200
|
||||
;插件管理页面账号,可选
|
||||
admin_user = admin
|
||||
;插件管理页面密码,与账号一起进行鉴权,可选
|
||||
admin_pwd = admin
|
||||
|
||||
[users]
|
||||
;user1
|
||||
user1 = 123
|
||||
;user2
|
||||
user2 = abc
|
||||
|
||||
[disabled]
|
||||
;user2被禁用
|
||||
user2 = disable
|
||||
```
|
||||
|
||||
每一个用户占一行,用户名和 token 之间以 `=` 号分隔。
|
||||
|
||||
2. 运行 frps-multiuser,指定监听地址以及 token 存储文件路径。
|
||||
|
||||
`./frps-multiuser -c ./frps-multiuser.ini`
|
||||
|
||||
3. 在 frps 的配置文件中注册插件,并启动。
|
||||
|
||||
```ini
|
||||
# frps.ini
|
||||
[common]
|
||||
bind_port = 7000
|
||||
|
||||
[plugin.multiuser]
|
||||
addr = 127.0.0.1:7200
|
||||
path = /handler
|
||||
ops = Login,NewWorkConn,NewUserConn,NewProxy,Ping
|
||||
```
|
||||
|
||||
4. 在 frpc 中指定用户名,在 meta 中指定 token,用户名以及 `meta_token` 的内容需要和之前创建的 token 文件匹配。
|
||||
|
||||
user1 的配置:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
user = user1
|
||||
meta_token = 123
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
```
|
||||
|
||||
user2 的配置:(由于示例文件中user2被禁用,因此无法连接)
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
user = user2
|
||||
meta_token = abc
|
||||
|
||||
[ssh]
|
||||
type = tcp
|
||||
local_port = 22
|
||||
remote_port = 6000
|
||||
```
|
||||
|
||||
# 致谢
|
||||
|
||||
+ [frp](https://github.com/fatedier/frp)
|
||||
+ [fp-multiuser](https://github.com/gofrp/fp-multiuser)
|
||||
+ [layui](https://github.com/layui/layui)
|
||||
48
assets/lang/en.json
Normal file
48
assets/lang/en.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"User Manage": "User Manage",
|
||||
"User": "User",
|
||||
"Token": "Token",
|
||||
"Notes": "Notes",
|
||||
"Search": "Search",
|
||||
"Reset": "Reset",
|
||||
"New user": "New user",
|
||||
"Remove user": "Remove user",
|
||||
"Disable user": "Disable user",
|
||||
"will take sometime to make effective": " will take sometime to make effective",
|
||||
"Enable user": "Enable user",
|
||||
"Remove": "Remove",
|
||||
"Disable": "Disable",
|
||||
"Enable": "Enable",
|
||||
"Please input user account": "Please input user account",
|
||||
"Please input user token": "Please input user token",
|
||||
"Please input user notes": "Please input user notes",
|
||||
"Status": "Status",
|
||||
"Operation": "Operation",
|
||||
"Confirm": "Confirm",
|
||||
"Cancel": "Cancel",
|
||||
"Confirm to remove user": "Confirm to remove user ?",
|
||||
"Confirm to disable user": "Confirm to disable user ?",
|
||||
"Confirm to enable user": "Confirm to enable user ?",
|
||||
"Operate success": "Operate success",
|
||||
"Operate failed": "Operate failed",
|
||||
"Operate error": "Operate error",
|
||||
"Other error": "Other error",
|
||||
"Param error": "Param error",
|
||||
"User exist": "User exist",
|
||||
"Token cannot be empty": "Token cannot be empty",
|
||||
"Please check at least one user": "Please Check at least one user",
|
||||
"Operation confirm": "Operation confirm",
|
||||
"Empty data": "Empty data",
|
||||
"Allowed ports": "Allowed ports",
|
||||
"Please input allowed ports": "Please input allowed ports, example: 8081, 9000-9010",
|
||||
"Allowed domains": "Allowed domains",
|
||||
"Please input allowed domains": "Please input allowed domains, example: web01.domain.com,web02.domain.com",
|
||||
"Allowed subdomains": "Allowed subdomains",
|
||||
"Please input allowed subdomains": "Please input allowed subdomains, example: web01,web02",
|
||||
"Ports is invalid": "Ports is invalid",
|
||||
"Domains is invalid": "Domains is invalid",
|
||||
"Subdomains is invalid": "Subdomains is invalid",
|
||||
"Comment is invalid": "Comment is invalid, it cannot include line breaks",
|
||||
"Not limit": "Not limit",
|
||||
"None": "None"
|
||||
}
|
||||
48
assets/lang/zh.json
Normal file
48
assets/lang/zh.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"User Manage": "用户管理",
|
||||
"User": "用户名(user)",
|
||||
"Token": "凭证(meta_token)",
|
||||
"Notes": "备注",
|
||||
"Search": "搜索",
|
||||
"Reset": "重置",
|
||||
"New user": "新增用户",
|
||||
"Remove user": "删除用户",
|
||||
"Disable user": "禁用用户",
|
||||
"will take sometime to make effective": "需要一定时间才会生效",
|
||||
"Enable user": "启用用户",
|
||||
"Remove": "删除",
|
||||
"Disable": "禁用",
|
||||
"Enable": "启用",
|
||||
"Please input user account": "请输入用户名(user)",
|
||||
"Please input user token": "请输入Token(meta_token)",
|
||||
"Please input user notes": "请输入备注",
|
||||
"Status": "状态",
|
||||
"Operation": "操作",
|
||||
"Confirm": "确定",
|
||||
"Cancel": "取消",
|
||||
"Confirm to remove user": "确定删除用户 ?",
|
||||
"Confirm to disable user": "确定禁用用户 ?",
|
||||
"Confirm to enable user": "确定启用用户 ?",
|
||||
"Operate success": "操作成功",
|
||||
"Operate failed": "操作失败",
|
||||
"Operate error": "操作异常",
|
||||
"Other error": "其他异常",
|
||||
"Param error": "参数异常",
|
||||
"User exist": "用户已经存在",
|
||||
"Token cannot be empty": "Token 不能为空",
|
||||
"Please check at least one user": "请选中需要操作的用户",
|
||||
"Operation confirm": "操作确认",
|
||||
"Empty data": "无数据",
|
||||
"Allowed ports": "允许端口",
|
||||
"Please input allowed ports": "请输入允许使用的端口,如:8081, 9000-9010",
|
||||
"Allowed domains": "允许域名",
|
||||
"Please input allowed domains": "请输入允许使用的域名,如:web01.domain.com,web02.domain.com",
|
||||
"Allowed subdomains": "允许子域名",
|
||||
"Please input allowed subdomains": "请输入允许使用的端口,如:web01,web02",
|
||||
"Ports is invalid": "端口不正确",
|
||||
"Domains is invalid": "域名不正确",
|
||||
"Subdomains is invalid": "子域名不正确",
|
||||
"Comment is invalid": "备注不正确,不能包含换行",
|
||||
"Not limit": "无限制",
|
||||
"None": "无"
|
||||
}
|
||||
53
assets/static/css/index.css
Normal file
53
assets/static/css/index.css
Normal file
@@ -0,0 +1,53 @@
|
||||
body {
|
||||
padding: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#searchForm input {
|
||||
height: 30px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
#searchForm .layui-input-suffix,
|
||||
#searchForm .layui-input-prefix {
|
||||
line-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#addUserForm {
|
||||
padding: 15px 15px 0 15px;
|
||||
}
|
||||
|
||||
#addUserForm .layui-form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#addUserForm .layui-textarea {
|
||||
min-height: 80px;
|
||||
padding: 9px 10px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.layui-form-label {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.layui-input-block {
|
||||
margin-left: 170px;
|
||||
}
|
||||
|
||||
.layui-btn-sm {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.layui-btn-xs {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.layui-btn-container{
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.layui-layer-btn > a[class^=layui-layer-btn]{
|
||||
line-height: 28px;
|
||||
}
|
||||
2186
assets/static/css/layui-theme-dark.css
Normal file
2186
assets/static/css/layui-theme-dark.css
Normal file
File diff suppressed because it is too large
Load Diff
563
assets/static/js/index.js
Normal file
563
assets/static/js/index.js
Normal file
@@ -0,0 +1,563 @@
|
||||
var $ = layui.$;
|
||||
$(function () {
|
||||
var apiType = {
|
||||
Remove: 1,
|
||||
Enable: 2,
|
||||
Disable: 3
|
||||
}
|
||||
|
||||
/**
|
||||
* verify comment is valid
|
||||
* @param comment
|
||||
*
|
||||
* @return {{valid:boolean, trim:string}}
|
||||
*/
|
||||
function verifyComment(comment) {
|
||||
var valid = true;
|
||||
if (comment.trim() !== '' && /[\n\t\r]/.test(comment)) {
|
||||
valid = false;
|
||||
}
|
||||
return {
|
||||
valid: valid,
|
||||
trim: comment.replace(/[\n\t\r]/g, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* verify ports is valid
|
||||
* @param ports
|
||||
*
|
||||
* @return {{valid:boolean, trim:string}}
|
||||
*/
|
||||
function verifyPorts(ports) {
|
||||
var valid = true;
|
||||
if (ports.trim() !== '') {
|
||||
try {
|
||||
ports.split(",").forEach(function (port) {
|
||||
if (/^\s*\d{1,5}\s*$/.test(port)) {
|
||||
if (parseInt(port) < 1 || parseInt(port) > 65535) {
|
||||
valid = false;
|
||||
}
|
||||
} else if (/^\s*\d{1,5}\s*-\s*\d{1,5}\s*$/.test(port)) {
|
||||
var portRange = port.split('-');
|
||||
if (parseInt(portRange[0]) < 1 || parseInt(portRange[0]) > 65535) {
|
||||
valid = false;
|
||||
} else if (parseInt(portRange[1]) < 1 || parseInt(portRange[1]) > 65535) {
|
||||
valid = false;
|
||||
} else if (parseInt(portRange[0]) > parseInt(portRange[1])) {
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
if (valid === false) {
|
||||
throw 'break';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: valid,
|
||||
trim: ports.replace(/\s/g, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* verify domains is valid
|
||||
* @param domains
|
||||
*
|
||||
* @return {{valid:boolean, trim:string}}
|
||||
*/
|
||||
function verifyDomains(domains) {
|
||||
var valid = true;
|
||||
if (domains.trim() !== '') {
|
||||
try {
|
||||
domains.split(',').forEach(function (domain) {
|
||||
if (!/^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62}){1,3}$/.test(domain.trim())) {
|
||||
valid = false;
|
||||
throw 'break';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: valid,
|
||||
trim: domains.replace(/\s/g, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* verify subdomains is valid
|
||||
* @param subdomains
|
||||
*
|
||||
* @return {{valid:boolean, trim:string}}
|
||||
*/
|
||||
function verifySubdomains(subdomains) {
|
||||
var valid = true;
|
||||
if (subdomains.trim() !== '') {
|
||||
try {
|
||||
subdomains.split(',').forEach(function (subdomain) {
|
||||
if (!/^[a-zA-z0-9][a-zA-Z0-9-]{0,19}$/.test(subdomain.trim())) {
|
||||
valid = false;
|
||||
throw 'break';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: valid,
|
||||
trim: subdomains.replace(/\s/g, '')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* load i18n language
|
||||
* @param lang {{}}
|
||||
*/
|
||||
function langLoaded(lang) {
|
||||
//set verify rules
|
||||
var verifyRules = {
|
||||
comment: function (value, item) {
|
||||
var result = verifyComment(value);
|
||||
if (!result.valid) {
|
||||
return lang['CommentInvalid'];
|
||||
}
|
||||
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 (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 (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 (typeof item === "function") {
|
||||
item && item(result.trim);
|
||||
} else {
|
||||
$(item).val(result.trim);
|
||||
}
|
||||
}
|
||||
};
|
||||
layui.form.verify(verifyRules);
|
||||
|
||||
layui.table.render({
|
||||
elem: '#tokenTable',
|
||||
url: '/tokens',
|
||||
method: 'get',
|
||||
where: {},
|
||||
dataType: 'json',
|
||||
editTrigger: 'dblclick',
|
||||
page: navigator.language.indexOf("zh") === 0,
|
||||
toolbar: '#toolbarTemplate',
|
||||
defaultToolbar: false,
|
||||
text: {none: lang['EmptyData']},
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'user', title: lang['User'], width: 150, sort: true},
|
||||
{field: 'token', title: lang['Token'], width: 200, sort: true, edit: true},
|
||||
{field: 'comment', title: lang['Notes'], sort: true, edit: 'textarea'},
|
||||
{field: 'ports', title: lang['AllowedPorts'], sort: true, edit: 'textarea'},
|
||||
{field: 'domains', title: lang['AllowedDomains'], sort: true, edit: 'textarea'},
|
||||
{field: 'subdomains', title: lang['AllowedSubdomains'], sort: true, edit: 'textarea'},
|
||||
{
|
||||
field: 'status',
|
||||
title: lang['Status'],
|
||||
width: 100,
|
||||
templet: '<span>{{d.status? "' + lang['Enable'] + '":"' + lang['Disable'] + '"}}</span>',
|
||||
sort: true
|
||||
},
|
||||
{title: lang['Operation'], width: 150, toolbar: '#operationTemplate'}
|
||||
]]
|
||||
});
|
||||
|
||||
/**
|
||||
* update layui table data
|
||||
* @param obj table update obj
|
||||
* @param field update field
|
||||
* @param trim new value
|
||||
*/
|
||||
function updateTableField(obj, field, trim) {
|
||||
var newData = {};
|
||||
newData[field] = trim;
|
||||
obj.update(newData);
|
||||
}
|
||||
|
||||
layui.table.on('edit(tokenTable)', function (obj) {
|
||||
var field = obj.field;
|
||||
var value = obj.value;
|
||||
var oldValue = obj.oldValue;
|
||||
var before = $.extend(true, {}, obj.data);
|
||||
var after = $.extend(true, {}, obj.data);
|
||||
var verifyMsg = false;
|
||||
if (field === 'token') {
|
||||
if (value.trim() === '') {
|
||||
layui.layer.msg(lang['TokenEmpty'])
|
||||
return obj.reedit();
|
||||
}
|
||||
|
||||
before.token = oldValue;
|
||||
after.token = value;
|
||||
} else if (field === 'comment') {
|
||||
verifyMsg = verifyRules.comment(value, function (trim) {
|
||||
updateTableField(obj, field, trim)
|
||||
});
|
||||
if (verifyMsg) {
|
||||
layui.layer.msg(verifyMsg);
|
||||
return obj.reedit();
|
||||
}
|
||||
|
||||
before.comment = oldValue;
|
||||
after.comment = value;
|
||||
} else if (field === 'ports') {
|
||||
verifyMsg = verifyRules.ports(value, function (trim) {
|
||||
updateTableField(obj, field, trim)
|
||||
});
|
||||
if (verifyMsg) {
|
||||
layui.layer.msg(verifyMsg);
|
||||
return obj.reedit();
|
||||
}
|
||||
|
||||
before.ports = oldValue;
|
||||
after.ports = value;
|
||||
} else if (field === 'domains') {
|
||||
verifyMsg = verifyRules.domains(value, function (trim) {
|
||||
updateTableField(obj, field, trim)
|
||||
});
|
||||
if (verifyMsg) {
|
||||
layui.layer.msg(verifyMsg);
|
||||
return obj.reedit();
|
||||
}
|
||||
|
||||
before.domains = oldValue;
|
||||
after.domains = value;
|
||||
} else if (field === 'subdomains') {
|
||||
verifyMsg = verifyRules.subdomains(value, function (trim) {
|
||||
updateTableField(obj, field, trim)
|
||||
});
|
||||
if (verifyMsg) {
|
||||
layui.layer.msg(verifyMsg);
|
||||
return obj.reedit();
|
||||
}
|
||||
|
||||
before.subdomains = oldValue;
|
||||
after.subdomains = value;
|
||||
}
|
||||
|
||||
update(before, after);
|
||||
});
|
||||
|
||||
layui.table.on('toolbar(tokenTable)', function (obj) {
|
||||
var id = obj.config.id;
|
||||
var checkStatus = layui.table.checkStatus(id);
|
||||
switch (obj.event) {
|
||||
case 'add':
|
||||
addPopup();
|
||||
break
|
||||
case 'remove':
|
||||
batchRemovePopup(checkStatus.data);
|
||||
break
|
||||
case 'disable':
|
||||
batchDisablePopup(checkStatus.data);
|
||||
break
|
||||
case 'enable':
|
||||
batchEnablePopup(checkStatus.data);
|
||||
break
|
||||
}
|
||||
});
|
||||
layui.table.on('tool(tokenTable)', function (obj) {
|
||||
var data = obj.data;
|
||||
switch (obj.event) {
|
||||
case 'remove':
|
||||
removePopup(data);
|
||||
break;
|
||||
case 'disable':
|
||||
disablePopup(data);
|
||||
break;
|
||||
case 'enable':
|
||||
enablePopup(data);
|
||||
break
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* add user popup
|
||||
*/
|
||||
function addPopup() {
|
||||
layui.layer.open({
|
||||
type: 1,
|
||||
title: lang['NewUser'],
|
||||
area: ['500px'],
|
||||
content: layui.laytpl(document.getElementById('addTemplate').innerHTML).render(),
|
||||
btn: [lang['Confirm'], lang['Cancel']],
|
||||
btn1: function (index) {
|
||||
if (layui.form.validate('#addUserForm')) {
|
||||
add(layui.form.val('addUserForm'), index)
|
||||
}
|
||||
},
|
||||
btn2: function (index) {
|
||||
layui.layer.close(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* add user action
|
||||
* @param data {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} user data
|
||||
* @param index popup index
|
||||
*/
|
||||
function add(data, index) {
|
||||
var loading = layui.layer.load();
|
||||
$.ajax({
|
||||
url: '/add',
|
||||
type: 'post',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function (result) {
|
||||
if (result.success) {
|
||||
reloadTable();
|
||||
layui.layer.close(index);
|
||||
layui.layer.msg(lang['OperateSuccess'], function (index) {
|
||||
layui.layer.close(index);
|
||||
});
|
||||
} else {
|
||||
errorMsg(result);
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
layui.layer.close(loading);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* update user action
|
||||
* @param before {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} data before update
|
||||
* @param after {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} data after update
|
||||
*/
|
||||
function update(before, after) {
|
||||
var loading = layui.layer.load();
|
||||
$.ajax({
|
||||
url: '/update',
|
||||
type: 'post',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
before: before,
|
||||
after: after,
|
||||
}),
|
||||
success: function (result) {
|
||||
if (result.success) {
|
||||
layui.layer.msg(lang['OperateSuccess']);
|
||||
} else {
|
||||
errorMsg(result);
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
layui.layer.close(loading);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* batch remove user popup
|
||||
* @param data {[{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}]} user data list
|
||||
*/
|
||||
function batchRemovePopup(data) {
|
||||
if (data.length === 0) {
|
||||
layui.layer.msg(lang['ShouldCheckUser']);
|
||||
return;
|
||||
}
|
||||
layui.layer.confirm(lang['ConfirmRemoveUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Remove, data, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* batch disable user popup
|
||||
* @param data {[{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}]} user data list
|
||||
*/
|
||||
function batchDisablePopup(data) {
|
||||
if (data.length === 0) {
|
||||
layui.layer.msg(lang['ShouldCheckUser']);
|
||||
return;
|
||||
}
|
||||
layui.layer.confirm(lang['ConfirmDisableUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Disable, data, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* batch enable user popup
|
||||
* @param data {[{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}]} user data list
|
||||
*/
|
||||
function batchEnablePopup(data) {
|
||||
if (data.length === 0) {
|
||||
layui.layer.msg(lang['ShouldCheckUser']);
|
||||
return;
|
||||
}
|
||||
layui.layer.confirm(lang['ConfirmEnableUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Enable, data, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* remove one user popup
|
||||
* @param data {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} user data
|
||||
*/
|
||||
function removePopup(data) {
|
||||
layui.layer.confirm(lang['ConfirmRemoveUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Remove, [data], index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* disable one user popup
|
||||
* @param data {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} user data
|
||||
*/
|
||||
function disablePopup(data) {
|
||||
layui.layer.confirm(lang['ConfirmDisableUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Disable, [data], index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* enable one user popup
|
||||
* @param data {{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}} user data
|
||||
*/
|
||||
function enablePopup(data) {
|
||||
layui.layer.confirm(lang['ConfirmEnableUser'], {
|
||||
title: lang['OperationConfirm'],
|
||||
btn: [lang['Confirm'], lang['Cancel']]
|
||||
}, function (index) {
|
||||
operate(apiType.Enable, [data], index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* operate actions
|
||||
* @param type {apiType} action type
|
||||
* @param data {[{user:string, token:string, comment:string, status:boolean, ports:string, domains:string, subdomains:string}]} user data list
|
||||
* @param index popup index
|
||||
*/
|
||||
function operate(type, data, index) {
|
||||
var url;
|
||||
var extendMessage = '';
|
||||
if (type === apiType.Remove) {
|
||||
url = "/remove";
|
||||
extendMessage = ', ' + lang['RemoveUser'] + lang['TakeTimeMakeEffective'];
|
||||
} else if (type === apiType.Disable) {
|
||||
url = "/disable";
|
||||
extendMessage = ', ' + lang['RemoveUser'] + lang['TakeTimeMakeEffective'];
|
||||
} else if (type === apiType.Enable) {
|
||||
url = "/enable";
|
||||
} else {
|
||||
layer.layer.msg(lang['OperateError']);
|
||||
return;
|
||||
}
|
||||
var loading = layui.layer.load();
|
||||
$.post({
|
||||
url: url,
|
||||
type: 'post',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
users: data
|
||||
}),
|
||||
success: function (result) {
|
||||
if (result.success) {
|
||||
reloadTable();
|
||||
layui.layer.close(index);
|
||||
layui.layer.msg(lang['OperateSuccess'] + extendMessage, function (index) {
|
||||
layui.layer.close(index);
|
||||
});
|
||||
} else {
|
||||
errorMsg(result);
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
layui.layer.close(loading);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* reload user table
|
||||
*/
|
||||
function reloadTable() {
|
||||
var searchData = layui.form.val('searchForm');
|
||||
layui.table.reloadData('tokenTable', {
|
||||
where: searchData
|
||||
}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* show error message popup
|
||||
* @param result
|
||||
*/
|
||||
function errorMsg(result) {
|
||||
var reason = lang['Other Error'];
|
||||
if (result.code === 1)
|
||||
reason = lang['ParamError'];
|
||||
else if (result.code === 2)
|
||||
reason = lang['UserExist'];
|
||||
layui.layer.msg(lang['OperateFailed'] + ',' + reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* click event
|
||||
*/
|
||||
$(document).on('click.search', '#searchBtn', function () {
|
||||
reloadTable();
|
||||
return false;
|
||||
}).on('click.reset', '#resetBtn', function () {
|
||||
$('#searchForm')[0].reset();
|
||||
reloadTable();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
var langLoading = layui.layer.load()
|
||||
$.getJSON('/lang').done(langLoaded).always(function () {
|
||||
layui.layer.close(langLoading);
|
||||
});
|
||||
});
|
||||
1
assets/static/layui/css/layui.css
Normal file
1
assets/static/layui/css/layui.css
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/static/layui/font/iconfont.eot
Normal file
BIN
assets/static/layui/font/iconfont.eot
Normal file
Binary file not shown.
405
assets/static/layui/font/iconfont.svg
Normal file
405
assets/static/layui/font/iconfont.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 322 KiB |
BIN
assets/static/layui/font/iconfont.ttf
Normal file
BIN
assets/static/layui/font/iconfont.ttf
Normal file
Binary file not shown.
BIN
assets/static/layui/font/iconfont.woff
Normal file
BIN
assets/static/layui/font/iconfont.woff
Normal file
Binary file not shown.
BIN
assets/static/layui/font/iconfont.woff2
Normal file
BIN
assets/static/layui/font/iconfont.woff2
Normal file
Binary file not shown.
1
assets/static/layui/layui.js
Normal file
1
assets/static/layui/layui.js
Normal file
File diff suppressed because one or more lines are too long
124
assets/templates/index.html
Normal file
124
assets/templates/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>${ .UserManage }</title>
|
||||
<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/index.css">
|
||||
<script src="./static/layui/layui.js"></script>
|
||||
<script src="./static/js/index.js"></script>
|
||||
<style>
|
||||
.layui-table-cell:empty::after {
|
||||
content: '${ .NotLimit }';
|
||||
}
|
||||
|
||||
td[data-field=comment] .layui-table-cell:empty::after {
|
||||
content: '${ .None }';
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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" value="" placeholder="${ .User }" class="layui-input" autocomplete="off"
|
||||
lay-affix="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-input-wrap">
|
||||
<div class="layui-input-prefix">
|
||||
<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>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-input-wrap">
|
||||
<div class="layui-input-prefix">
|
||||
<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>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-btn-container">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table id="tokenTable" lay-filter="tokenTable"></table>
|
||||
<script type="text/html" id="toolbarTemplate">
|
||||
<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="remove">${ .RemoveUser }</button>
|
||||
<button class="layui-btn layui-btn-sm" lay-event="disable">${ .DisableUser }</button>
|
||||
<button class="layui-btn layui-btn-sm" lay-event="enable">${ .EnableUser }</button>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="operationTemplate">
|
||||
<div class="layui-clear-space">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="remove">${ .Remove }</a>
|
||||
{{# if (d.status) { }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="disable">${ .Disable }</a>
|
||||
{{# } else { }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="enable">${ .Enable }</a>
|
||||
{{# } }}
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="addTemplate">
|
||||
<div 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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">${ .Notes }</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="comment" lay-verify="comment" placeholder="${ .PleaseInputUserNotes }"
|
||||
autocomplete="off" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">${ .AllowedPorts }</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="ports" lay-verify="ports" placeholder="${ .PleaseInputAllowedPorts }"
|
||||
autocomplete="off" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">${ .AllowedDomains }</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="domains" lay-verify="domains" placeholder="${ .PleaseInputAllowedDomains }"
|
||||
autocomplete="off" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">${ .AllowedSubdomains }</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="subdomains" lay-verify="subdomains" placeholder="${ .PleaseInputAllowedSubdomains }"
|
||||
autocomplete="off" class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
182
cmd/fp-multiuser/cmd.go
Normal file
182
cmd/fp-multiuser/cmd.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"frps-multiuser/pkg/server"
|
||||
"frps-multiuser/pkg/server/controller"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/ini.v1"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const version = "0.0.2"
|
||||
|
||||
var (
|
||||
showVersion bool
|
||||
configFile string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps-multiuser")
|
||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "./frps-multiuser.ini", "config file of frps-multiuser")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "frps-multiuser",
|
||||
Short: "frps-multiuser is the server plugin of frp to support multiple users.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if showVersion {
|
||||
log.Println(version)
|
||||
return nil
|
||||
}
|
||||
common, tokens, ports, domains, subdomains, iniFile, err := ParseConfigFile(configFile)
|
||||
if err != nil {
|
||||
log.Printf("fail to start frps-multiuser : %v", err)
|
||||
return nil
|
||||
}
|
||||
s, err := server.New(controller.HandleController{
|
||||
CommonInfo: common,
|
||||
Tokens: tokens,
|
||||
Ports: ports,
|
||||
Domains: domains,
|
||||
Subdomains: subdomains,
|
||||
ConfigFile: configFile,
|
||||
IniFile: iniFile,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseConfigFile(file string) (controller.CommonInfo, map[string]controller.TokenInfo, map[string][]string, map[string][]string, map[string][]string, *ini.File, error) {
|
||||
common := controller.CommonInfo{}
|
||||
users := make(map[string]controller.TokenInfo)
|
||||
ports := make(map[string][]string)
|
||||
domains := make(map[string][]string)
|
||||
subdomains := make(map[string][]string)
|
||||
|
||||
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
||||
Insensitive: false,
|
||||
InsensitiveSections: false,
|
||||
InsensitiveKeys: false,
|
||||
IgnoreInlineComment: true,
|
||||
AllowBooleanKeys: true,
|
||||
}, file)
|
||||
if err != nil {
|
||||
var pathError *fs.PathError
|
||||
if errors.As(err, &pathError) {
|
||||
log.Printf("token file %s not found", file)
|
||||
} else {
|
||||
log.Printf("fail to parse token file %s : %v", file, err)
|
||||
}
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
|
||||
commonSection, err := iniFile.GetSection("common")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [common] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
pluginAddr := commonSection.Key("plugin_addr").Value()
|
||||
if len(pluginAddr) != 0 {
|
||||
common.PluginAddr = pluginAddr
|
||||
} else {
|
||||
common.PluginAddr = "0.0.0.0"
|
||||
}
|
||||
pluginPort := commonSection.Key("plugin_port").Value()
|
||||
if len(pluginPort) != 0 {
|
||||
port, err := strconv.Atoi(pluginPort)
|
||||
if err != nil {
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
common.PluginPort = port
|
||||
} else {
|
||||
common.PluginPort = 7200
|
||||
}
|
||||
common.User = commonSection.Key("admin_user").Value()
|
||||
common.Pwd = commonSection.Key("admin_pwd").Value()
|
||||
|
||||
portsSection, err := iniFile.GetSection("ports")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [ports] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
for _, key := range portsSection.Keys() {
|
||||
user := key.Name()
|
||||
value := key.Value()
|
||||
port := strings.Split(controller.TrimAllSpaceReg.ReplaceAllString(value, ""), ",")
|
||||
ports[user] = port
|
||||
}
|
||||
|
||||
domainsSection, err := iniFile.GetSection("domains")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [domains] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
for _, key := range domainsSection.Keys() {
|
||||
user := key.Name()
|
||||
value := key.Value()
|
||||
domain := strings.Split(controller.TrimAllSpaceReg.ReplaceAllString(value, ""), ",")
|
||||
domains[user] = domain
|
||||
}
|
||||
|
||||
subdomainsSection, err := iniFile.GetSection("subdomains")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [subdomains] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
for _, key := range subdomainsSection.Keys() {
|
||||
user := key.Name()
|
||||
value := key.Value()
|
||||
subdomain := strings.Split(controller.TrimAllSpaceReg.ReplaceAllString(value, ""), ",")
|
||||
subdomains[user] = subdomain
|
||||
}
|
||||
|
||||
usersSection, err := iniFile.GetSection("users")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [users] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
|
||||
disabledSection, err := iniFile.GetSection("disabled")
|
||||
if err != nil {
|
||||
log.Printf("fail to get [disabled] section from file %s : %v", file, err)
|
||||
return common, nil, nil, nil, nil, iniFile, err
|
||||
}
|
||||
|
||||
keys := usersSection.Keys()
|
||||
for _, key := range keys {
|
||||
comment, found := strings.CutPrefix(key.Comment, ";")
|
||||
if !found {
|
||||
comment, found = strings.CutPrefix(comment, "#")
|
||||
}
|
||||
token := controller.TokenInfo{
|
||||
User: key.Name(),
|
||||
Token: key.Value(),
|
||||
Comment: comment,
|
||||
Ports: strings.Join(ports[key.Name()], ","),
|
||||
Domains: strings.Join(domains[key.Name()], ","),
|
||||
Subdomains: strings.Join(subdomains[key.Name()], ","),
|
||||
Status: !(disabledSection.HasKey(key.Name()) && disabledSection.Key(key.Name()).Value() == "disable"),
|
||||
}
|
||||
users[token.User] = token
|
||||
}
|
||||
|
||||
return common, users, ports, domains, subdomains, iniFile, nil
|
||||
}
|
||||
5
cmd/fp-multiuser/main.go
Normal file
5
cmd/fp-multiuser/main.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
Execute()
|
||||
}
|
||||
22
config/frps-multiuser.ini
Normal file
22
config/frps-multiuser.ini
Normal file
@@ -0,0 +1,22 @@
|
||||
; basic options
|
||||
[common]
|
||||
plugin_addr = 127.0.0.1
|
||||
plugin_port = 7200
|
||||
admin_user = admin
|
||||
admin_pwd = admin
|
||||
|
||||
; user tokens
|
||||
[users]
|
||||
user1 = token1
|
||||
|
||||
; user been disabled
|
||||
[disabled]
|
||||
|
||||
; user allowed ports. it will be used when a new proxy connect on
|
||||
[ports]
|
||||
|
||||
; user allowed domains. it will be used when a new proxy connect on
|
||||
[domains]
|
||||
|
||||
; user allowed subdomains. it will be used when a new proxy connect on
|
||||
[subdomains]
|
||||
44
go.mod
Normal file
44
go.mod
Normal file
@@ -0,0 +1,44 @@
|
||||
module frps-multiuser
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/fatedier/frp v0.34.1
|
||||
github.com/gin-contrib/i18n v1.0.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/spf13/cobra v0.0.3
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.10.0-rc3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb // indirect
|
||||
github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
321
go.sum
Normal file
321
go.sum
Normal file
@@ -0,0 +1,321 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
|
||||
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
|
||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
|
||||
github.com/fatedier/frp v0.34.1 h1:8J0ASuKVqo/IQubIVkMnuuflzhNT661PexUgeye/zUE=
|
||||
github.com/fatedier/frp v0.34.1/go.mod h1:y3PpthszJ+S5HB2J5kcXqoLr5rtgdmMa0RuvnGqGfik=
|
||||
github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185 h1:2p4W5xYizIYwhiGQgeHOQcRD2O84j0tjD40P6gUCRrk=
|
||||
github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185/go.mod h1:MUs+IH/MGJNz5Cj2JVJBPZBKw2exON7LzO3HrJHmGiQ=
|
||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/i18n v1.0.0 h1:e5uEOmaAr09Iyr4vuWuvvpByjmvxGDO7iSkmiFpSsk0=
|
||||
github.com/gin-contrib/i18n v1.0.0/go.mod h1:yyyTArpVZeXCFT/kbLbD5CS192+OZ8Y+angnJjvnB98=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.9.1/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.12.3/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc/go.mod h1:6/gX3+E/IYGa0wMORlSMla999awQFdbaeQCHjSMKIzY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rakyll/statik v0.1.1/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
|
||||
github.com/rodaine/table v1.0.0/go.mod h1:YAUzwPOji0DUJNEvggdxyQcUAl4g3hDRcFlyjnnR51I=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
|
||||
github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
|
||||
github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
12
package.sh
Normal file
12
package.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
cd ./release || exit
|
||||
rm -rf *.zip
|
||||
list=$(ls frps-multiuser-*)
|
||||
echo "$list"
|
||||
for binFile in $list
|
||||
do
|
||||
cp "$binFile" frps-multiuser
|
||||
zip -r "$binFile".zip frps-multiuser frps-multiuser.ini assets -x "*.git*" "*.idea*" "*.DS_Store" "*.contentFlavour"
|
||||
rm -rf "$binFile" frps-multiuser
|
||||
done
|
||||
rm -rf frps-multiuser.ini assets
|
||||
598
pkg/server/controller/controller.go
Normal file
598
pkg/server/controller/controller.go
Normal file
@@ -0,0 +1,598 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Success = 0
|
||||
ParamError = 1
|
||||
UserExist = 2
|
||||
SaveError = 3
|
||||
)
|
||||
|
||||
var TrimAllSpaceReg = regexp.MustCompile("[\\n\\t\\r\\s]")
|
||||
var TrimBreakLineReg = regexp.MustCompile("[\\n\\t\\r]")
|
||||
|
||||
type Response struct {
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type HTTPError struct {
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
type CommonInfo struct {
|
||||
PluginAddr string
|
||||
PluginPort int
|
||||
User string
|
||||
Pwd string
|
||||
}
|
||||
|
||||
type TokenInfo struct {
|
||||
User string `json:"user" form:"user"`
|
||||
Token string `json:"token" form:"token"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Ports string `json:"ports" from:"ports"`
|
||||
Domains string `json:"domains" from:"domains"`
|
||||
Subdomains string `json:"subdomains" from:"subdomains"`
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Count int `json:"count"`
|
||||
Data []TokenInfo `json:"data"`
|
||||
}
|
||||
|
||||
type OperationResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type TokenSearch struct {
|
||||
TokenInfo
|
||||
Page int `form:"page"`
|
||||
Limit int `form:"limit"`
|
||||
}
|
||||
|
||||
type TokenUpdate struct {
|
||||
Before TokenInfo `json:"before"`
|
||||
After TokenInfo `json:"after"`
|
||||
}
|
||||
|
||||
type TokenRemove struct {
|
||||
Users []TokenInfo `json:"users"`
|
||||
}
|
||||
|
||||
type TokenDisable struct {
|
||||
TokenRemove
|
||||
}
|
||||
|
||||
type TokenEnable struct {
|
||||
TokenDisable
|
||||
}
|
||||
|
||||
func (e *HTTPError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
type HandlerFunc func(ctx *gin.Context) (interface{}, error)
|
||||
|
||||
func (c *HandleController) MakeHandlerFunc() gin.HandlerFunc {
|
||||
return func(context *gin.Context) {
|
||||
var response plugin.Response
|
||||
var err error
|
||||
|
||||
request := plugin.Request{}
|
||||
if err := context.BindJSON(&request); err != nil {
|
||||
_ = context.Error(&HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
jsonStr, err := json.Marshal(request.Content)
|
||||
if err != nil {
|
||||
_ = context.Error(&HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if request.Op == "Login" {
|
||||
content := plugin.LoginContent{}
|
||||
err = json.Unmarshal(jsonStr, &content)
|
||||
response = c.HandleLogin(&content)
|
||||
} else if request.Op == "NewProxy" {
|
||||
content := plugin.NewProxyContent{}
|
||||
err = json.Unmarshal(jsonStr, &content)
|
||||
response = c.HandleNewProxy(&content)
|
||||
} else if request.Op == "Ping" {
|
||||
content := plugin.PingContent{}
|
||||
err = json.Unmarshal(jsonStr, &content)
|
||||
response = c.HandlePing(&content)
|
||||
} else if request.Op == "NewWorkConn" {
|
||||
content := plugin.NewWorkConnContent{}
|
||||
err = json.Unmarshal(jsonStr, &content)
|
||||
response = c.HandleNewWorkConn(&content)
|
||||
} else if request.Op == "NewUserConn" {
|
||||
content := plugin.NewUserConnContent{}
|
||||
err = json.Unmarshal(jsonStr, &content)
|
||||
response = c.HandleNewUserConn(&content)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("handle %s error: %v", context.Request.URL.Path, err)
|
||||
var e *HTTPError
|
||||
switch {
|
||||
case errors.As(err, &e):
|
||||
context.JSON(e.Code, &Response{Msg: e.Err.Error()})
|
||||
default:
|
||||
context.JSON(http.StatusInternalServerError, &Response{Msg: err.Error()})
|
||||
}
|
||||
return
|
||||
} else {
|
||||
resStr, _ := json.Marshal(response)
|
||||
log.Printf("handle:%v , result: %v", request.Op, string(resStr))
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeManagerFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
context.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"UserManage": ginI18n.MustGetMessage(context, "User Manage"),
|
||||
"User": ginI18n.MustGetMessage(context, "User"),
|
||||
"Token": ginI18n.MustGetMessage(context, "Token"),
|
||||
"Notes": ginI18n.MustGetMessage(context, "Notes"),
|
||||
"Search": ginI18n.MustGetMessage(context, "Search"),
|
||||
"Reset": ginI18n.MustGetMessage(context, "Reset"),
|
||||
"NewUser": ginI18n.MustGetMessage(context, "New user"),
|
||||
"RemoveUser": ginI18n.MustGetMessage(context, "Remove user"),
|
||||
"DisableUser": ginI18n.MustGetMessage(context, "Disable user"),
|
||||
"EnableUser": ginI18n.MustGetMessage(context, "Enable user"),
|
||||
"Remove": ginI18n.MustGetMessage(context, "Remove"),
|
||||
"Enable": ginI18n.MustGetMessage(context, "Enable"),
|
||||
"Disable": ginI18n.MustGetMessage(context, "Disable"),
|
||||
"PleaseInputUserAccount": ginI18n.MustGetMessage(context, "Please input user account"),
|
||||
"PleaseInputUserToken": ginI18n.MustGetMessage(context, "Please input user token"),
|
||||
"PleaseInputUserNotes": ginI18n.MustGetMessage(context, "Please input user notes"),
|
||||
"AllowedPorts": ginI18n.MustGetMessage(context, "Allowed ports"),
|
||||
"PleaseInputAllowedPorts": ginI18n.MustGetMessage(context, "Please input allowed ports"),
|
||||
"AllowedDomains": ginI18n.MustGetMessage(context, "Allowed domains"),
|
||||
"PleaseInputAllowedDomains": ginI18n.MustGetMessage(context, "Please input allowed domains"),
|
||||
"AllowedSubdomains": ginI18n.MustGetMessage(context, "Allowed subdomains"),
|
||||
"PleaseInputAllowedSubdomains": ginI18n.MustGetMessage(context, "Please input allowed subdomains"),
|
||||
"NotLimit": ginI18n.MustGetMessage(context, "Not limit"),
|
||||
"None": ginI18n.MustGetMessage(context, "None"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeLangFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
context.JSON(http.StatusOK, gin.H{
|
||||
"User": ginI18n.MustGetMessage(context, "User"),
|
||||
"Token": ginI18n.MustGetMessage(context, "Token"),
|
||||
"Notes": ginI18n.MustGetMessage(context, "Notes"),
|
||||
"Status": ginI18n.MustGetMessage(context, "Status"),
|
||||
"Operation": ginI18n.MustGetMessage(context, "Operation"),
|
||||
"Enable": ginI18n.MustGetMessage(context, "Enable"),
|
||||
"Disable": ginI18n.MustGetMessage(context, "Disable"),
|
||||
"NewUser": ginI18n.MustGetMessage(context, "New user"),
|
||||
"Confirm": ginI18n.MustGetMessage(context, "Confirm"),
|
||||
"Cancel": ginI18n.MustGetMessage(context, "Cancel"),
|
||||
"RemoveUser": ginI18n.MustGetMessage(context, "Remove user"),
|
||||
"DisableUser": ginI18n.MustGetMessage(context, "Disable user"),
|
||||
"ConfirmRemoveUser": ginI18n.MustGetMessage(context, "Confirm to remove user"),
|
||||
"ConfirmDisableUser": ginI18n.MustGetMessage(context, "Confirm to disable user"),
|
||||
"TakeTimeMakeEffective": ginI18n.MustGetMessage(context, "will take sometime to make effective"),
|
||||
"ConfirmEnableUser": ginI18n.MustGetMessage(context, "Confirm to enable user"),
|
||||
"OperateSuccess": ginI18n.MustGetMessage(context, "Operate success"),
|
||||
"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"),
|
||||
"ShouldCheckUser": ginI18n.MustGetMessage(context, "Please check at least one user"),
|
||||
"OperationConfirm": ginI18n.MustGetMessage(context, "Operation confirm"),
|
||||
"EmptyData": ginI18n.MustGetMessage(context, "Empty data"),
|
||||
"AllowedPorts": ginI18n.MustGetMessage(context, "Allowed ports"),
|
||||
"AllowedDomains": ginI18n.MustGetMessage(context, "Allowed domains"),
|
||||
"AllowedSubdomains": ginI18n.MustGetMessage(context, "Allowed subdomains"),
|
||||
"PortsInvalid": ginI18n.MustGetMessage(context, "Ports is invalid"),
|
||||
"DomainsInvalid": ginI18n.MustGetMessage(context, "Domains is invalid"),
|
||||
"SubdomainsInvalid": ginI18n.MustGetMessage(context, "Subdomains is invalid"),
|
||||
"CommentInvalid": ginI18n.MustGetMessage(context, "Comment is invalid"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeQueryTokensFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
|
||||
search := TokenSearch{}
|
||||
search.Limit = 0
|
||||
|
||||
err := context.BindQuery(&search)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var tokenList []TokenInfo
|
||||
for _, tokenInfo := range c.Tokens {
|
||||
tokenList = append(tokenList, tokenInfo)
|
||||
}
|
||||
sort.Slice(tokenList, func(i, j int) bool {
|
||||
return strings.Compare(tokenList[i].User, tokenList[j].User) < 0
|
||||
})
|
||||
|
||||
var filtered []TokenInfo
|
||||
for _, tokenInfo := range tokenList {
|
||||
if filter(tokenInfo, search.TokenInfo) {
|
||||
filtered = append(filtered, tokenInfo)
|
||||
}
|
||||
}
|
||||
if filtered == nil {
|
||||
filtered = []TokenInfo{}
|
||||
}
|
||||
|
||||
count := len(filtered)
|
||||
if search.Limit > 0 {
|
||||
start := max((search.Page-1)*search.Limit, 0)
|
||||
end := min(search.Page*search.Limit, len(filtered))
|
||||
filtered = filtered[start:end]
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, &TokenResponse{
|
||||
Code: 0,
|
||||
Msg: "query Tokens success",
|
||||
Count: count,
|
||||
Data: filtered,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func filter(main TokenInfo, sub TokenInfo) bool {
|
||||
replaceSpaceUser := TrimAllSpaceReg.ReplaceAllString(sub.User, "")
|
||||
if len(replaceSpaceUser) != 0 {
|
||||
if !strings.Contains(main.User, replaceSpaceUser) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
replaceSpaceToken := TrimAllSpaceReg.ReplaceAllString(sub.Token, "")
|
||||
if len(replaceSpaceToken) != 0 {
|
||||
if !strings.Contains(main.Token, replaceSpaceToken) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
replaceSpaceComment := TrimAllSpaceReg.ReplaceAllString(sub.Comment, "")
|
||||
if len(replaceSpaceComment) != 0 {
|
||||
if !strings.Contains(main.Comment, replaceSpaceComment) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeAddTokenFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
info := TokenInfo{
|
||||
Status: true,
|
||||
}
|
||||
response := OperationResponse{
|
||||
Success: true,
|
||||
Code: Success,
|
||||
Message: "user add success",
|
||||
}
|
||||
err := context.BindJSON(&info)
|
||||
if err != nil {
|
||||
log.Printf("user add failed, param error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = ParamError
|
||||
response.Message = "user add failed, param 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
|
||||
response.Code = UserExist
|
||||
response.Message = fmt.Sprintf("user add failed, user [%s] exist ", info.User)
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
c.Tokens[info.User] = info
|
||||
|
||||
usersSection, _ := c.IniFile.GetSection("users")
|
||||
key, err := usersSection.NewKey(info.User, info.Token)
|
||||
key.Comment = info.Comment
|
||||
|
||||
replaceSpacePorts := TrimAllSpaceReg.ReplaceAllString(info.Ports, "")
|
||||
if len(replaceSpacePorts) != 0 {
|
||||
portsSection, _ := c.IniFile.GetSection("ports")
|
||||
key, err = portsSection.NewKey(info.User, replaceSpacePorts)
|
||||
key.Comment = fmt.Sprintf("user %s allowed ports", info.User)
|
||||
}
|
||||
|
||||
replaceSpaceDomains := TrimAllSpaceReg.ReplaceAllString(info.Domains, "")
|
||||
if len(replaceSpaceDomains) != 0 {
|
||||
domainsSection, _ := c.IniFile.GetSection("domains")
|
||||
key, err = domainsSection.NewKey(info.User, replaceSpaceDomains)
|
||||
key.Comment = fmt.Sprintf("user %s allowed domains", info.User)
|
||||
}
|
||||
|
||||
replaceSpaceSubdomains := TrimAllSpaceReg.ReplaceAllString(info.Subdomains, "")
|
||||
if len(replaceSpaceSubdomains) != 0 {
|
||||
subdomainsSection, _ := c.IniFile.GetSection("subdomains")
|
||||
key, err = subdomainsSection.NewKey(info.User, replaceSpaceSubdomains)
|
||||
key.Comment = fmt.Sprintf("user %s allowed subdomains", info.User)
|
||||
}
|
||||
|
||||
err = c.IniFile.SaveTo(c.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("add failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "user add failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
context.JSON(0, &response)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeUpdateTokensFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
response := OperationResponse{
|
||||
Success: true,
|
||||
Code: Success,
|
||||
Message: "user update success",
|
||||
}
|
||||
update := TokenUpdate{}
|
||||
err := context.BindJSON(&update)
|
||||
if err != nil {
|
||||
log.Printf("update failed, param error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = ParamError
|
||||
response.Message = "user update failed, param error "
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
after := update.After
|
||||
before := update.Before
|
||||
|
||||
usersSection, _ := c.IniFile.GetSection("users")
|
||||
key, err := usersSection.GetKey(before.User)
|
||||
comment := TrimBreakLineReg.ReplaceAllString(after.Comment, "")
|
||||
after.Comment = comment
|
||||
key.Comment = comment
|
||||
key.SetValue(after.Token)
|
||||
|
||||
if before.Ports != after.Ports {
|
||||
portsSection, _ := c.IniFile.GetSection("ports")
|
||||
replaceSpacePorts := TrimAllSpaceReg.ReplaceAllString(after.Ports, "")
|
||||
after.Ports = replaceSpacePorts
|
||||
ports := strings.Split(replaceSpacePorts, ",")
|
||||
if len(replaceSpacePorts) != 0 {
|
||||
key, err = portsSection.NewKey(after.User, replaceSpacePorts)
|
||||
key.Comment = fmt.Sprintf("user %s allowed ports", after.User)
|
||||
c.Ports[after.User] = ports
|
||||
} else {
|
||||
portsSection.DeleteKey(after.User)
|
||||
delete(c.Ports, after.User)
|
||||
}
|
||||
}
|
||||
|
||||
if before.Domains != after.Domains {
|
||||
domainsSection, _ := c.IniFile.GetSection("domains")
|
||||
replaceSpaceDomains := TrimAllSpaceReg.ReplaceAllString(after.Domains, "")
|
||||
after.Domains = replaceSpaceDomains
|
||||
domains := strings.Split(replaceSpaceDomains, ",")
|
||||
if len(replaceSpaceDomains) != 0 {
|
||||
key, err = domainsSection.NewKey(after.User, replaceSpaceDomains)
|
||||
key.Comment = fmt.Sprintf("user %s allowed domains", after.User)
|
||||
c.Domains[after.User] = domains
|
||||
} else {
|
||||
domainsSection.DeleteKey(after.User)
|
||||
delete(c.Domains, after.User)
|
||||
}
|
||||
}
|
||||
|
||||
if before.Subdomains != after.Subdomains {
|
||||
subdomainsSection, _ := c.IniFile.GetSection("subdomains")
|
||||
replaceSpaceSubdomains := TrimAllSpaceReg.ReplaceAllString(after.Subdomains, "")
|
||||
after.Subdomains = replaceSpaceSubdomains
|
||||
subdomains := strings.Split(replaceSpaceSubdomains, ",")
|
||||
if len(replaceSpaceSubdomains) != 0 {
|
||||
key, err = subdomainsSection.NewKey(after.User, replaceSpaceSubdomains)
|
||||
key.Comment = fmt.Sprintf("user %s allowed subdomains", after.User)
|
||||
c.Subdomains[after.User] = subdomains
|
||||
} else {
|
||||
subdomainsSection.DeleteKey(after.User)
|
||||
delete(c.Subdomains, after.User)
|
||||
}
|
||||
}
|
||||
|
||||
c.Tokens[after.User] = after
|
||||
|
||||
err = c.IniFile.SaveTo(c.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("user update failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "user update failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeRemoveTokensFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
response := OperationResponse{
|
||||
Success: true,
|
||||
Code: Success,
|
||||
Message: "user remove success",
|
||||
}
|
||||
remove := TokenRemove{}
|
||||
err := context.BindJSON(&remove)
|
||||
if err != nil {
|
||||
log.Printf("user remove failed, param error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = ParamError
|
||||
response.Message = "user remove failed, param error "
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
usersSection, _ := c.IniFile.GetSection("users")
|
||||
for _, user := range remove.Users {
|
||||
delete(c.Tokens, user.User)
|
||||
usersSection.DeleteKey(user.User)
|
||||
}
|
||||
|
||||
portsSection, _ := c.IniFile.GetSection("ports")
|
||||
for _, user := range remove.Users {
|
||||
delete(c.Ports, user.User)
|
||||
portsSection.DeleteKey(user.User)
|
||||
}
|
||||
|
||||
domainsSection, _ := c.IniFile.GetSection("domains")
|
||||
for _, user := range remove.Users {
|
||||
delete(c.Domains, user.User)
|
||||
domainsSection.DeleteKey(user.User)
|
||||
}
|
||||
|
||||
subdomainsSection, _ := c.IniFile.GetSection("subdomains")
|
||||
for _, user := range remove.Users {
|
||||
delete(c.Subdomains, user.User)
|
||||
subdomainsSection.DeleteKey(user.User)
|
||||
}
|
||||
|
||||
err = c.IniFile.SaveTo(c.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("user remove failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "user remove failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeDisableTokensFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
response := OperationResponse{
|
||||
Success: true,
|
||||
Code: Success,
|
||||
Message: "remove success",
|
||||
}
|
||||
disable := TokenDisable{}
|
||||
err := context.BindJSON(&disable)
|
||||
if err != nil {
|
||||
log.Printf("disable failed, param error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = ParamError
|
||||
response.Message = "disable failed, param error "
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
section, _ := c.IniFile.GetSection("disabled")
|
||||
for _, user := range disable.Users {
|
||||
section.DeleteKey(user.User)
|
||||
token := c.Tokens[user.User]
|
||||
token.Status = false
|
||||
c.Tokens[user.User] = token
|
||||
key, err := section.NewKey(user.User, "disable")
|
||||
if err != nil {
|
||||
log.Printf("disable failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "disable failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
key.Comment = fmt.Sprintf("disable user '%s'", user.User)
|
||||
}
|
||||
|
||||
err = c.IniFile.SaveTo(c.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("disable failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "disable failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HandleController) MakeEnableTokensFunc() func(context *gin.Context) {
|
||||
return func(context *gin.Context) {
|
||||
response := OperationResponse{
|
||||
Success: true,
|
||||
Code: Success,
|
||||
Message: "remove success",
|
||||
}
|
||||
enable := TokenEnable{}
|
||||
err := context.BindJSON(&enable)
|
||||
if err != nil {
|
||||
log.Printf("enable failed, param error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = ParamError
|
||||
response.Message = "enable failed, param error "
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
section, _ := c.IniFile.GetSection("disabled")
|
||||
for _, user := range enable.Users {
|
||||
section.DeleteKey(user.User)
|
||||
token := c.Tokens[user.User]
|
||||
token.Status = true
|
||||
c.Tokens[user.User] = token
|
||||
}
|
||||
|
||||
err = c.IniFile.SaveTo(c.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("enable failed, error : %v", err)
|
||||
response.Success = false
|
||||
response.Code = SaveError
|
||||
response.Message = "enable failed"
|
||||
context.JSON(http.StatusOK, &response)
|
||||
return
|
||||
}
|
||||
|
||||
context.JSON(http.StatusOK, &response)
|
||||
}
|
||||
}
|
||||
234
pkg/server/controller/op.go
Normal file
234
pkg/server/controller/op.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/server"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/ini.v1"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HandleController struct {
|
||||
CommonInfo CommonInfo
|
||||
Tokens map[string]TokenInfo
|
||||
Ports map[string][]string
|
||||
Domains map[string][]string
|
||||
Subdomains map[string][]string
|
||||
ConfigFile string
|
||||
IniFile *ini.File
|
||||
}
|
||||
|
||||
func NewHandleController(config *HandleController) *HandleController {
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *HandleController) Register(engine *gin.Engine) {
|
||||
engine.Delims("${", "}")
|
||||
engine.LoadHTMLGlob("./assets/templates/*")
|
||||
engine.POST("/handler", c.MakeHandlerFunc())
|
||||
|
||||
var group *gin.RouterGroup
|
||||
if len(c.CommonInfo.User) != 0 {
|
||||
group = engine.Group("/", gin.BasicAuthForRealm(gin.Accounts{
|
||||
c.CommonInfo.User: c.CommonInfo.Pwd,
|
||||
}, "Restricted"))
|
||||
} else {
|
||||
group = engine.Group("/")
|
||||
}
|
||||
group.Static("/static", "./assets/static")
|
||||
group.GET("/", c.MakeManagerFunc())
|
||||
group.GET("/lang", c.MakeLangFunc())
|
||||
group.GET("/tokens", c.MakeQueryTokensFunc())
|
||||
group.POST("/add", c.MakeAddTokenFunc())
|
||||
group.POST("/update", c.MakeUpdateTokensFunc())
|
||||
group.POST("/remove", c.MakeRemoveTokensFunc())
|
||||
group.POST("/disable", c.MakeDisableTokensFunc())
|
||||
group.POST("/enable", c.MakeEnableTokensFunc())
|
||||
}
|
||||
|
||||
func (c *HandleController) HandleLogin(content *plugin.LoginContent) plugin.Response {
|
||||
token := content.Metas["token"]
|
||||
user := content.User
|
||||
return c.JudgeToken(user, token)
|
||||
}
|
||||
|
||||
func (c *HandleController) HandleNewProxy(content *plugin.NewProxyContent) plugin.Response {
|
||||
token := content.User.Metas["token"]
|
||||
user := content.User.User
|
||||
judgeToken := c.JudgeToken(user, token)
|
||||
if judgeToken.Reject {
|
||||
return judgeToken
|
||||
}
|
||||
return c.JudgePort(content)
|
||||
}
|
||||
|
||||
func (c *HandleController) HandlePing(content *plugin.PingContent) plugin.Response {
|
||||
token := content.User.Metas["token"]
|
||||
user := content.User.User
|
||||
return c.JudgeToken(user, token)
|
||||
}
|
||||
|
||||
func (c *HandleController) HandleNewWorkConn(content *plugin.NewWorkConnContent) plugin.Response {
|
||||
token := content.User.Metas["token"]
|
||||
user := content.User.User
|
||||
return c.JudgeToken(user, token)
|
||||
}
|
||||
|
||||
func (c *HandleController) HandleNewUserConn(content *plugin.NewUserConnContent) plugin.Response {
|
||||
token := content.User.Metas["token"]
|
||||
user := content.User.User
|
||||
return c.JudgeToken(user, token)
|
||||
}
|
||||
|
||||
func (c *HandleController) JudgeToken(user string, token string) plugin.Response {
|
||||
var res plugin.Response
|
||||
if len(c.Tokens) == 0 {
|
||||
res.Unchange = true
|
||||
} else if user == "" || token == "" {
|
||||
res.Reject = true
|
||||
res.RejectReason = "user or meta token can not be empty"
|
||||
} else if info, exist := c.Tokens[user]; exist {
|
||||
if !info.Status {
|
||||
res.Reject = true
|
||||
res.RejectReason = fmt.Sprintf("user [%s] is disabled", user)
|
||||
} else {
|
||||
if info.Token != token {
|
||||
res.Reject = true
|
||||
res.RejectReason = fmt.Sprintf("invalid meta token for user [%s]", user)
|
||||
} else {
|
||||
res.Unchange = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.Reject = true
|
||||
res.RejectReason = fmt.Sprintf("user [%s] not exist", user)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *HandleController) JudgePort(content *plugin.NewProxyContent) plugin.Response {
|
||||
var res plugin.Response
|
||||
var portErr error
|
||||
var reject = false
|
||||
supportProxyTypes := []string{
|
||||
"tcp", "tcpmux", "udp", "http", "https",
|
||||
}
|
||||
proxyType := content.ProxyType
|
||||
|
||||
if StringIndexOf(proxyType, supportProxyTypes) == -1 {
|
||||
log.Printf("proxy type [%v] not support, plugin do nothing", proxyType)
|
||||
res.Unchange = true
|
||||
return res
|
||||
}
|
||||
|
||||
user := content.User.User
|
||||
userPort := content.RemotePort
|
||||
userDomains := content.CustomDomains
|
||||
userSubdomain := content.SubDomain
|
||||
|
||||
portAllowed := true
|
||||
if proxyType == "tcp" || proxyType == "udp" {
|
||||
portAllowed = false
|
||||
if _, exist := c.Ports[user]; exist {
|
||||
for _, port := range c.Ports[user] {
|
||||
if strings.Contains(port, "-") {
|
||||
allowedRanges := strings.Split(port, "-")
|
||||
if len(allowedRanges) != 2 {
|
||||
portErr = fmt.Errorf("user [%v] port range [%v] format error", user, port)
|
||||
break
|
||||
}
|
||||
start, err := strconv.Atoi(strings.TrimSpace(allowedRanges[0]))
|
||||
if err != nil {
|
||||
portErr = fmt.Errorf("user [%v] port rang [%v] start port [%v] is not a number", user, port, allowedRanges[0])
|
||||
break
|
||||
}
|
||||
end, err := strconv.Atoi(strings.TrimSpace(allowedRanges[1]))
|
||||
if err != nil {
|
||||
portErr = fmt.Errorf("user [%v] port rang [%v] end port [%v] is not a number", user, port, allowedRanges[0])
|
||||
break
|
||||
}
|
||||
if max(userPort, start) == userPort && min(userPort, end) == userPort {
|
||||
portAllowed = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
allowed, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
portErr = fmt.Errorf("user [%v] allowed port [%v] is not a number", user, port)
|
||||
}
|
||||
if allowed == userPort {
|
||||
portAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
portAllowed = true
|
||||
}
|
||||
}
|
||||
if !portAllowed {
|
||||
if portErr == nil {
|
||||
portErr = fmt.Errorf("user [%v] port [%v] is not allowed", user, userPort)
|
||||
}
|
||||
reject = true
|
||||
}
|
||||
|
||||
domainAllowed := true
|
||||
if proxyType == "http" || proxyType == "https" || proxyType == "tcpmux" {
|
||||
if portAllowed {
|
||||
if _, exist := c.Domains[user]; exist {
|
||||
for _, userDomain := range userDomains {
|
||||
if StringIndexOf(userDomain, c.Domains[user]) == -1 {
|
||||
domainAllowed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !domainAllowed {
|
||||
portErr = fmt.Errorf("user [%v] domain [%v] is not allowed", user, strings.Join(userDomains, ","))
|
||||
reject = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subdomainAllowed := true
|
||||
if proxyType == "http" || proxyType == "https" {
|
||||
subdomainAllowed = false
|
||||
if portAllowed && domainAllowed {
|
||||
if _, exist := c.Subdomains[user]; exist {
|
||||
for _, subdomain := range c.Subdomains[user] {
|
||||
if subdomain == userSubdomain {
|
||||
subdomainAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subdomainAllowed = true
|
||||
}
|
||||
if !subdomainAllowed {
|
||||
portErr = fmt.Errorf("user [%v] subdomain [%v] is not allowed", user, userSubdomain)
|
||||
reject = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reject {
|
||||
res.Reject = true
|
||||
res.RejectReason = portErr.Error()
|
||||
} else {
|
||||
res.Unchange = true
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func StringIndexOf(element string, data []string) int {
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
142
pkg/server/server.go
Normal file
142
pkg/server/server.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"frps-multiuser/pkg/server/controller"
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/text/language"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
cfg controller.HandleController
|
||||
s *http.Server
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func New(cfg controller.HandleController) (*Server, error) {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if err := s.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
bindAddress := s.cfg.CommonInfo.PluginAddr + ":" + strconv.Itoa(s.cfg.CommonInfo.PluginPort)
|
||||
l, err := net.Listen("tcp", bindAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("HTTP server listen on %s", l.Addr().String())
|
||||
go func() {
|
||||
if err = s.s.Serve(l); !errors.Is(http.ErrServerClosed, err) {
|
||||
log.Printf("error shutdown HTTP server: %v", err)
|
||||
}
|
||||
}()
|
||||
<-s.done
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := s.s.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("shutdown HTTP server error: %v", err)
|
||||
}
|
||||
log.Printf("HTTP server exited")
|
||||
close(s.done)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) init() error {
|
||||
if err := s.initHTTPServer(); err != nil {
|
||||
log.Printf("init HTTP server error: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadSupportLanguage(dir string) ([]language.Tag, error) {
|
||||
var tags []language.Tag
|
||||
|
||||
files, err := os.Open(dir)
|
||||
if err != nil {
|
||||
log.Printf("error opening directory: %v", err)
|
||||
return tags, err
|
||||
}
|
||||
|
||||
fileList, err := files.Readdir(-1)
|
||||
if err != nil {
|
||||
log.Printf("error reading directory: %v", err)
|
||||
return tags, err
|
||||
}
|
||||
|
||||
err = files.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range fileList {
|
||||
name, _ := strings.CutSuffix(file.Name(), ".json")
|
||||
parsedLang, _ := language.Parse(name)
|
||||
tags = append(tags, parsedLang)
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
return tags, fmt.Errorf("not found any language file in directory: %v", dir)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func GinI18nLocalize() gin.HandlerFunc {
|
||||
dir := "./assets/lang"
|
||||
tags, err := LoadSupportLanguage(dir)
|
||||
if err != nil {
|
||||
log.Panicf("language file is not found: %v", err)
|
||||
}
|
||||
return ginI18n.Localize(
|
||||
ginI18n.WithBundle(&ginI18n.BundleCfg{
|
||||
RootPath: dir,
|
||||
AcceptLanguage: tags,
|
||||
DefaultLanguage: language.Chinese,
|
||||
FormatBundleFile: "json",
|
||||
UnmarshalFunc: json.Unmarshal,
|
||||
}),
|
||||
ginI18n.WithGetLngHandle(
|
||||
func(context *gin.Context, defaultLng string) string {
|
||||
header := context.GetHeader("Accept-Language")
|
||||
lang, _, err := language.ParseAcceptLanguage(header)
|
||||
if err != nil {
|
||||
return defaultLng
|
||||
}
|
||||
return lang[0].String()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) initHTTPServer() error {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(GinI18nLocalize())
|
||||
s.s = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
controller.NewHandleController(&s.cfg).Register(engine)
|
||||
return nil
|
||||
}
|
||||
BIN
screenshots/dark mode.png
Normal file
BIN
screenshots/dark mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
screenshots/i18n.png
Normal file
BIN
screenshots/i18n.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
screenshots/new user.png
Normal file
BIN
screenshots/new user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
BIN
screenshots/user list.png
Normal file
BIN
screenshots/user list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Reference in New Issue
Block a user