Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2948eaaa5c | ||
|
|
8641ba5c0c | ||
|
|
60b7a18fe7 | ||
|
|
ca068816ae | ||
|
|
06648d9a6c | ||
|
|
8a8abd5163 | ||
|
|
97f98cd6ce | ||
|
|
51f2920661 | ||
|
|
7a5d141ce8 | ||
|
|
3cef02a0bb | ||
|
|
46a7ecc1ba | ||
|
|
4d2b037f5e | ||
|
|
323364b24e | ||
|
|
f19109cdf8 | ||
|
|
527260d60a | ||
|
|
46bb44f0ab | ||
|
|
2f1380f24a | ||
|
|
ece3328e94 | ||
|
|
fdd26d87be | ||
|
|
2ade0dda42 | ||
|
|
a87ae5cf65 | ||
|
|
fe7b8b53a6 | ||
|
|
b929f3efdb | ||
|
|
f847fc076f | ||
|
|
60d0a701ce | ||
|
|
0dedaf6824 | ||
|
|
ab231b3fed | ||
|
|
e7f28cca36 | ||
|
|
505e8aac4b | ||
|
|
746e2a6052 | ||
|
|
dc03d5d83d | ||
|
|
b770ab178d | ||
|
|
fd7e022e88 |
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
- name: Set up Go environment
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22' # 选择 Go 版本
|
||||
go-version: '1.23' # 选择 Go 版本
|
||||
|
||||
- name: Set up npm
|
||||
uses: actions/setup-node@v2
|
||||
@@ -115,12 +115,12 @@ jobs:
|
||||
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
|
||||
else
|
||||
if [ "${{ matrix.job.platform }}" = "arm64" ]; then
|
||||
wget https://musl.cc/aarch64-linux-musl-cross.tgz
|
||||
wget https://musl.ljw.red/aarch64-linux-musl-cross.tgz
|
||||
tar -xf aarch64-linux-musl-cross.tgz
|
||||
export PATH=$PATH:$PWD/aarch64-linux-musl-cross/bin
|
||||
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=aarch64-linux-musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
|
||||
elif [ "${{ matrix.job.platform }}" = "armv7l" ]; then
|
||||
wget https://musl.cc/armv7l-linux-musleabihf-cross.tgz
|
||||
wget https://musl.ljw.red/armv7l-linux-musleabihf-cross.tgz
|
||||
tar -xf armv7l-linux-musleabihf-cross.tgz
|
||||
export PATH=$PATH:$PWD/armv7l-linux-musleabihf-cross/bin
|
||||
GOOS=${{ matrix.job.goos }} GOARCH=arm GOARM=7 CC=armv7l-linux-musleabihf-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
|
||||
@@ -147,6 +147,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate Changelog
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
|
||||
run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -380,7 +381,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest Docker Hub (:version)
|
||||
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
|
||||
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
|
||||
@@ -390,7 +391,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest GHCR (:version)
|
||||
if: ${{ env.SKIP_GHCR == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
|
||||
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
|
||||
@@ -401,7 +402,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest Docker Hub (:latest)
|
||||
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
|
||||
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
|
||||
@@ -411,7 +412,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest GHCR (:latest)
|
||||
if: ${{ env.SKIP_GHCR == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
|
||||
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
|
||||
@@ -422,7 +423,7 @@ jobs:
|
||||
|
||||
- name: Create and push Full S6 manifest Docker Hub (:version)
|
||||
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6
|
||||
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64,
|
||||
@@ -433,7 +434,7 @@ jobs:
|
||||
|
||||
- name: Create and push Full S6 manifest GHCR (:latest)
|
||||
if: ${{ env.SKIP_GHCR == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6
|
||||
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64,
|
||||
|
||||
10
.github/workflows/build_test.yml
vendored
10
.github/workflows/build_test.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Set up Go environment
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22' # 选择 Go 版本
|
||||
go-version: '1.23' # 选择 Go 版本
|
||||
|
||||
- name: Set up npm
|
||||
uses: actions/setup-node@v2
|
||||
@@ -101,12 +101,12 @@ jobs:
|
||||
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.${{matrix.job.file_ext}} ./release
|
||||
else
|
||||
if [ "${{ matrix.job.platform }}" = "arm64" ]; then
|
||||
wget https://musl.cc/aarch64-linux-musl-cross.tgz
|
||||
wget https://musl.ljw.red/aarch64-linux-musl-cross.tgz
|
||||
tar -xf aarch64-linux-musl-cross.tgz
|
||||
export PATH=$PATH:$PWD/aarch64-linux-musl-cross/bin
|
||||
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=aarch64-linux-musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
|
||||
elif [ "${{ matrix.job.platform }}" = "armv7l" ]; then
|
||||
wget https://musl.cc/armv7l-linux-musleabihf-cross.tgz
|
||||
wget https://musl.ljw.red/armv7l-linux-musleabihf-cross.tgz
|
||||
tar -xf armv7l-linux-musleabihf-cross.tgz
|
||||
export PATH=$PATH:$PWD/armv7l-linux-musleabihf-cross/bin
|
||||
GOOS=${{ matrix.job.goos }} GOARCH=arm GOARM=7 CC=armv7l-linux-musleabihf-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
|
||||
@@ -317,7 +317,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest Docker Hub (:version)
|
||||
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
|
||||
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
|
||||
- name: Create and push manifest GHCR (:version)
|
||||
if: ${{ env.SKIP_GHCR == 'false' }}
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@v0.2.3
|
||||
with:
|
||||
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
|
||||
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@ runtime/*
|
||||
go.sum
|
||||
resources/admin
|
||||
release
|
||||
data
|
||||
data/rustdeskapi.db
|
||||
@@ -76,7 +76,6 @@ COPY --from=builder-backend /app/release /app/
|
||||
COPY --from=builder-backend /app/conf /app/conf/
|
||||
COPY --from=builder-backend /app/resources /app/resources/
|
||||
COPY --from=builder-backend /app/docs /app/docs/
|
||||
COPY --from=builder-backend /app/http/templates /app/http/templates
|
||||
# Copy frontend build from builder2 stage
|
||||
COPY --from=builder-admin-frontend /frontend/dist/ /app/resources/admin/
|
||||
|
||||
|
||||
@@ -163,6 +163,9 @@
|
||||
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
|
||||
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `168h` |
|
||||
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | 是否禁用密码登录; `true`, `false` 默认`false` | `false` |
|
||||
| RUSTDESK_API_APP_REGISTER_STATUS | 注册用户默认状态; 1 启用,2 禁用, 默认 1 | `1` |
|
||||
| RUSTDESK_API_APP_CAPTCHA_THRESHOLD | 验证码触发次数; -1 不启用, 0 一直启用, >0 登录错误次数后启用 ;默认 `3` | `3` |
|
||||
| RUSTDESK_API_APP_BAN_THRESHOLD | 封禁IP触发次数; 0 不启用, >0 登录错误次数后封禁IP; 默认 `0` | `0` |
|
||||
| -----ADMIN配置----- | ---------- | ---------- |
|
||||
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
|
||||
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
|
||||
|
||||
@@ -162,6 +162,9 @@ The table below does not list all configurations. Please refer to the configurat
|
||||
| RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
|
||||
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration | `168h` |
|
||||
| RUSTDESK_API_APP_DISABLE_PWD_LOGIN | disable password login | `false` |
|
||||
| RUSTDESK_API_APP_REGISTER_STATUS | register user default status ; 1 enabled , 2 disabled ; default 1 | `1` |
|
||||
| RUSTDESK_API_APP_CAPTCHA_THRESHOLD | captcha threshold; -1 disabled, 0 always enable, >0 threshold ;default `3` | `3` |
|
||||
| RUSTDESK_API_APP_BAN_THRESHOLD | ban ip threshold; 0 disabled, >0 threshold ; default `0` | `0` |
|
||||
| ----- ADMIN Configuration----- | ---------- | ---------- |
|
||||
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
|
||||
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/lejianwen/rustdesk-api/v2/config"
|
||||
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// @title 管理系统API
|
||||
@@ -139,18 +141,40 @@ func InitGlobal() {
|
||||
}
|
||||
//gorm
|
||||
if global.Config.Gorm.Type == config.TypeMysql {
|
||||
dns := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/" + global.Config.Mysql.Dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
global.Config.Mysql.Username,
|
||||
global.Config.Mysql.Password,
|
||||
global.Config.Mysql.Addr,
|
||||
global.Config.Mysql.Dbname,
|
||||
)
|
||||
|
||||
global.DB = orm.NewMysql(&orm.MysqlConfig{
|
||||
Dns: dns,
|
||||
Dsn: dsn,
|
||||
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
|
||||
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
|
||||
})
|
||||
}, global.Logger)
|
||||
} else if global.Config.Gorm.Type == config.TypePostgresql {
|
||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
|
||||
global.Config.Postgresql.Host,
|
||||
global.Config.Postgresql.Port,
|
||||
global.Config.Postgresql.User,
|
||||
global.Config.Postgresql.Password,
|
||||
global.Config.Postgresql.Dbname,
|
||||
global.Config.Postgresql.Sslmode,
|
||||
global.Config.Postgresql.TimeZone,
|
||||
)
|
||||
global.DB = orm.NewPostgresql(&orm.PostgresqlConfig{
|
||||
Dsn: dsn,
|
||||
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
|
||||
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
|
||||
}, global.Logger)
|
||||
} else {
|
||||
//sqlite
|
||||
global.DB = orm.NewSqlite(&orm.SqliteConfig{
|
||||
MaxIdleConns: global.Config.Gorm.MaxIdleConns,
|
||||
MaxOpenConns: global.Config.Gorm.MaxOpenConns,
|
||||
})
|
||||
}, global.Logger)
|
||||
}
|
||||
|
||||
//validator
|
||||
@@ -175,8 +199,16 @@ func InitGlobal() {
|
||||
//service
|
||||
service.New(&global.Config, global.DB, global.Logger, global.Jwt, global.Lock)
|
||||
|
||||
global.LoginLimiter = utils.NewLoginLimiter(utils.SecurityPolicy{
|
||||
CaptchaThreshold: global.Config.App.CaptchaThreshold,
|
||||
BanThreshold: global.Config.App.BanThreshold,
|
||||
AttemptsWindow: 10 * time.Minute,
|
||||
BanDuration: 30 * time.Minute,
|
||||
})
|
||||
global.LoginLimiter.RegisterProvider(utils.B64StringCaptchaProvider{})
|
||||
DatabaseAutoUpdate()
|
||||
}
|
||||
|
||||
func DatabaseAutoUpdate() {
|
||||
version := 262
|
||||
|
||||
@@ -188,11 +220,17 @@ func DatabaseAutoUpdate() {
|
||||
if dbName == "" {
|
||||
dbName = global.Config.Mysql.Dbname
|
||||
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
|
||||
dsnWithoutDB := global.Config.Mysql.Username + ":" + global.Config.Mysql.Password + "@(" + global.Config.Mysql.Addr + ")/?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
dsnWithoutDB := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
global.Config.Mysql.Username,
|
||||
global.Config.Mysql.Password,
|
||||
global.Config.Mysql.Addr,
|
||||
"",
|
||||
)
|
||||
|
||||
//新链接
|
||||
dbWithoutDB := orm.NewMysql(&orm.MysqlConfig{
|
||||
Dns: dsnWithoutDB,
|
||||
})
|
||||
Dsn: dsnWithoutDB,
|
||||
}, global.Logger)
|
||||
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
|
||||
sqlDBWithoutDB, err := dbWithoutDB.DB()
|
||||
if err != nil {
|
||||
|
||||
@@ -2,14 +2,21 @@ lang: "zh-CN"
|
||||
app:
|
||||
web-client: 1 # 1:启用 0:禁用
|
||||
register: false #是否开启注册
|
||||
register-status: 1 # 注册用户默认状态 1:启用 2:禁用
|
||||
captcha-threshold: 3 # <0:disabled, 0 always, >0:enabled
|
||||
ban-threshold: 0 # 0:disabled, >0:enabled
|
||||
show-swagger: 0 # 1:启用 0:禁用
|
||||
token-expire: 168h
|
||||
web-sso: true #web auth sso
|
||||
disable-pwd-login: false #禁用密码登录
|
||||
|
||||
admin:
|
||||
title: "RustDesk Api Admin"
|
||||
title: "RustDesk API Admin"
|
||||
hello-file: "./conf/admin/hello.html" #优先使用file
|
||||
hello: ""
|
||||
# ID Server and Relay Server ports https://github.com/lejianwen/rustdesk-api/issues/257
|
||||
id-server-port: 21116 # ID Server port (for server cmd)
|
||||
relay-server-port: 21117 # ID Server port (for server cmd)
|
||||
gin:
|
||||
api-addr: "0.0.0.0:21114"
|
||||
mode: "release" #release,debug,test
|
||||
@@ -24,6 +31,16 @@ mysql:
|
||||
password: ""
|
||||
addr: ""
|
||||
dbname: ""
|
||||
|
||||
postgresql:
|
||||
host: "127.0.0.1"
|
||||
port: "5432"
|
||||
user: ""
|
||||
password: ""
|
||||
dbname: "postgres"
|
||||
sslmode: "disable" # disable, require, verify-ca, verify-full
|
||||
time-zone: "Asia/Shanghai" # Time zone for PostgreSQL connection
|
||||
|
||||
rustdesk:
|
||||
id-server: "192.168.1.66:21116"
|
||||
relay-server: "192.168.1.66:21117"
|
||||
@@ -64,21 +81,3 @@ ldap:
|
||||
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
|
||||
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
|
||||
|
||||
redis:
|
||||
addr: "127.0.0.1:6379"
|
||||
password: ""
|
||||
db: 0
|
||||
cache:
|
||||
type: "file"
|
||||
file-dir: "./runtime/cache"
|
||||
redis-addr: "127.0.0.1:6379"
|
||||
redis-pwd: ""
|
||||
redis-db: 0
|
||||
oss:
|
||||
access-key-id: ""
|
||||
access-key-secret: ""
|
||||
host: ""
|
||||
callback-url: ""
|
||||
expire-time: 30
|
||||
max-byte: 10240
|
||||
|
||||
|
||||
@@ -14,33 +14,48 @@ const (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
WebClient int `mapstructure:"web-client"`
|
||||
Register bool `mapstructure:"register"`
|
||||
ShowSwagger int `mapstructure:"show-swagger"`
|
||||
TokenExpire time.Duration `mapstructure:"token-expire"`
|
||||
WebSso bool `mapstructure:"web-sso"`
|
||||
DisablePwdLogin bool `mapstructure:"disable-pwd-login"`
|
||||
WebClient int `mapstructure:"web-client"`
|
||||
Register bool `mapstructure:"register"`
|
||||
RegisterStatus int `mapstructure:"register-status"`
|
||||
ShowSwagger int `mapstructure:"show-swagger"`
|
||||
TokenExpire time.Duration `mapstructure:"token-expire"`
|
||||
WebSso bool `mapstructure:"web-sso"`
|
||||
DisablePwdLogin bool `mapstructure:"disable-pwd-login"`
|
||||
CaptchaThreshold int `mapstructure:"captcha-threshold"`
|
||||
BanThreshold int `mapstructure:"ban-threshold"`
|
||||
}
|
||||
type Admin struct {
|
||||
Title string `mapstructure:"title"`
|
||||
Hello string `mapstructure:"hello"`
|
||||
HelloFile string `mapstructure:"hello-file"`
|
||||
Title string `mapstructure:"title"`
|
||||
Hello string `mapstructure:"hello"`
|
||||
HelloFile string `mapstructure:"hello-file"`
|
||||
IdServerPort int `mapstructure:"id-server-port"`
|
||||
RelayServerPort int `mapstructure:"relay-server-port"`
|
||||
}
|
||||
type Config struct {
|
||||
Lang string `mapstructure:"lang"`
|
||||
App App
|
||||
Admin Admin
|
||||
Gorm Gorm
|
||||
Mysql Mysql
|
||||
Gin Gin
|
||||
Logger Logger
|
||||
Redis Redis
|
||||
Cache Cache
|
||||
Oss Oss
|
||||
Jwt Jwt
|
||||
Rustdesk Rustdesk
|
||||
Proxy Proxy
|
||||
Ldap Ldap
|
||||
Lang string `mapstructure:"lang"`
|
||||
App App
|
||||
Admin Admin
|
||||
Gorm Gorm
|
||||
Mysql Mysql
|
||||
Postgresql Postgresql
|
||||
Gin Gin
|
||||
Logger Logger
|
||||
Redis Redis
|
||||
Cache Cache
|
||||
Oss Oss
|
||||
Jwt Jwt
|
||||
Rustdesk Rustdesk
|
||||
Proxy Proxy
|
||||
Ldap Ldap
|
||||
}
|
||||
|
||||
func (a *Admin) Init() {
|
||||
if a.IdServerPort == 0 {
|
||||
a.IdServerPort = DefaultIdServerPort
|
||||
}
|
||||
if a.RelayServerPort == 0 {
|
||||
a.RelayServerPort = DefaultRelayServerPort
|
||||
}
|
||||
}
|
||||
|
||||
// Init 初始化配置
|
||||
@@ -77,7 +92,7 @@ func Init(rowVal *Config, path string) *viper.Viper {
|
||||
panic(fmt.Errorf("Fatal error config: %s \n", err))
|
||||
}
|
||||
rowVal.Rustdesk.LoadKeyFile()
|
||||
rowVal.Rustdesk.ParsePort()
|
||||
rowVal.Admin.Init()
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
TypeSqlite = "sqlite"
|
||||
TypeMysql = "mysql"
|
||||
TypeSqlite = "sqlite"
|
||||
TypeMysql = "mysql"
|
||||
TypePostgresql = "postgresql"
|
||||
)
|
||||
|
||||
type Gorm struct {
|
||||
@@ -17,3 +18,13 @@ type Mysql struct {
|
||||
Password string `mapstructure:"password"`
|
||||
Dbname string `mapstructure:"dbname"`
|
||||
}
|
||||
|
||||
type Postgresql struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port string `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Dbname string `mapstructure:"dbname"`
|
||||
Sslmode string `mapstructure:"sslmode"` // "disable", "require", "verify-ca", "verify-full"
|
||||
TimeZone string `mapstructure:"time-zone"` // e.g., "Asia/Shanghai"
|
||||
}
|
||||
|
||||
@@ -18,3 +18,9 @@ type OidcOauth struct {
|
||||
ClientSecret string `mapstructure:"client-secret"`
|
||||
RedirectUrl string `mapstructure:"redirect-url"`
|
||||
}
|
||||
|
||||
type LinuxdoOauth struct {
|
||||
ClientId string `mapstructure:"client-id"`
|
||||
ClientSecret string `mapstructure:"client-secret"`
|
||||
RedirectUrl string `mapstructure:"redirect-url"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,19 +38,3 @@ func (rd *Rustdesk) LoadKeyFile() {
|
||||
return
|
||||
}
|
||||
}
|
||||
func (rd *Rustdesk) ParsePort() {
|
||||
// Parse port
|
||||
idres := strings.Split(rd.IdServer, ":")
|
||||
if len(idres) == 1 {
|
||||
rd.IdServerPort = DefaultIdServerPort
|
||||
} else if len(idres) == 2 {
|
||||
rd.IdServerPort, _ = strconv.Atoi(idres[1])
|
||||
}
|
||||
|
||||
relayres := strings.Split(rd.RelayServer, ":")
|
||||
if len(relayres) == 1 {
|
||||
rd.RelayServerPort = DefaultRelayServerPort
|
||||
} else if len(relayres) == 2 {
|
||||
rd.RelayServerPort, _ = strconv.Atoi(relayres[1])
|
||||
}
|
||||
}
|
||||
|
||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
@@ -5828,6 +5828,9 @@ const docTemplateadmin = `{
|
||||
"captcha": {
|
||||
"type": "string"
|
||||
},
|
||||
"captcha_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -5821,6 +5821,9 @@
|
||||
"captcha": {
|
||||
"type": "string"
|
||||
},
|
||||
"captcha_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -297,6 +297,8 @@ definitions:
|
||||
properties:
|
||||
captcha:
|
||||
type: string
|
||||
captcha_id:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
platform:
|
||||
|
||||
@@ -1208,7 +1208,7 @@ const docTemplateapi = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"地址"
|
||||
"System"
|
||||
],
|
||||
"summary": "提交系统信息",
|
||||
"parameters": [
|
||||
@@ -1238,6 +1238,35 @@ const docTemplateapi = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sysinfo_ver": {
|
||||
"post": {
|
||||
"description": "获取系统版本信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "获取系统版本信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
||||
@@ -1201,7 +1201,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"地址"
|
||||
"System"
|
||||
],
|
||||
"summary": "提交系统信息",
|
||||
"parameters": [
|
||||
@@ -1231,6 +1231,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sysinfo_ver": {
|
||||
"post": {
|
||||
"description": "获取系统版本信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "获取系统版本信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
||||
@@ -973,7 +973,26 @@ paths:
|
||||
$ref: '#/definitions/response.ErrorResponse'
|
||||
summary: 提交系统信息
|
||||
tags:
|
||||
- 地址
|
||||
- System
|
||||
/sysinfo_ver:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 获取系统版本信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.ErrorResponse'
|
||||
summary: 获取系统版本信息
|
||||
tags:
|
||||
- System
|
||||
/users:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
en_translations "github.com/go-playground/validator/v10/translations/en"
|
||||
es_translations "github.com/go-playground/validator/v10/translations/es"
|
||||
fr_translations "github.com/go-playground/validator/v10/translations/fr"
|
||||
ko_translations "github.com/go-playground/validator/v10/translations/ko"
|
||||
ru_translations "github.com/go-playground/validator/v10/translations/ru"
|
||||
zh_translations "github.com/go-playground/validator/v10/translations/zh"
|
||||
zh_tw_translations "github.com/go-playground/validator/v10/translations/zh_tw"
|
||||
@@ -51,8 +52,7 @@ func ApiInitValidator() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//validate没有ko的翻译,使用zh的翻译
|
||||
err = zh_translations.RegisterDefaultTranslations(validate, koTrans)
|
||||
err = ko_translations.RegisterDefaultTranslations(validate, koTrans)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/lejianwen/rustdesk-api/v2/lib/jwt"
|
||||
"github.com/lejianwen/rustdesk-api/v2/lib/lock"
|
||||
"github.com/lejianwen/rustdesk-api/v2/lib/upload"
|
||||
"github.com/lejianwen/rustdesk-api/v2/utils"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
@@ -31,8 +32,9 @@ var (
|
||||
ValidStruct func(*gin.Context, interface{}) []string
|
||||
ValidVar func(ctx *gin.Context, field interface{}, tag string) []string
|
||||
}
|
||||
Oss *upload.Oss
|
||||
Jwt *jwt.Jwt
|
||||
Lock lock.Locker
|
||||
Localizer func(lang string) *i18n.Localizer
|
||||
Oss *upload.Oss
|
||||
Jwt *jwt.Jwt
|
||||
Lock lock.Locker
|
||||
Localizer func(lang string) *i18n.Localizer
|
||||
LoginLimiter *utils.LoginLimiter
|
||||
)
|
||||
|
||||
36
go.mod
36
go.mod
@@ -1,19 +1,23 @@
|
||||
module github.com/lejianwen/rustdesk-api/v2
|
||||
|
||||
go 1.22
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.10
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/antonfisher/nested-logrus-formatter v1.3.1
|
||||
github.com/fsnotify/fsnotify v1.5.1
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.11.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mojocn/base64Captcha v1.3.6
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
@@ -22,10 +26,11 @@ require (
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/text v0.22.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.7
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,12 +41,12 @@ require (
|
||||
github.com/bytedance/sonic v1.8.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
@@ -51,12 +56,16 @@ require (
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
@@ -64,9 +73,9 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.4.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mojocn/base64Captcha v1.3.6 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
@@ -75,11 +84,12 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.63.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -120,7 +120,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(t *model.AddressBookCollectionR
|
||||
//check to_id
|
||||
if t.Type == model.ShareAddressBookRuleTypePersonal {
|
||||
if t.ToId == t.UserId {
|
||||
return "ParamsError", false
|
||||
return "CannotShareToSelf", false
|
||||
}
|
||||
tou := service.AllService.UserService.InfoById(t.ToId)
|
||||
if tou.Id == 0 {
|
||||
@@ -135,7 +135,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(t *model.AddressBookCollectionR
|
||||
return "ParamsError", false
|
||||
}
|
||||
// 重复检查
|
||||
ex := service.AllService.AddressBookService.RulePersonalInfoByToIdAndCid(t.ToId, t.CollectionId)
|
||||
ex := service.AllService.AddressBookService.RuleInfoByToIdAndCid(t.Type, t.ToId, t.CollectionId)
|
||||
if t.Id == 0 && ex.Id > 0 {
|
||||
return "ItemExists", false
|
||||
}
|
||||
|
||||
@@ -78,11 +78,13 @@ func (co *Config) AdminConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
hello := global.Config.Admin.Hello
|
||||
helloFile := global.Config.Admin.HelloFile
|
||||
if helloFile != "" {
|
||||
b, err := os.ReadFile(helloFile)
|
||||
if err == nil && len(b) > 0 {
|
||||
hello = string(b)
|
||||
if hello == "" {
|
||||
helloFile := global.Config.Admin.HelloFile
|
||||
if helloFile != "" {
|
||||
b, err := os.ReadFile(helloFile)
|
||||
if err == nil && len(b) > 0 {
|
||||
hello = string(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,135 +11,11 @@ import (
|
||||
adResp "github.com/lejianwen/rustdesk-api/v2/http/response/admin"
|
||||
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
}
|
||||
|
||||
// Captcha 验证码结构
|
||||
type Captcha struct {
|
||||
Id string `json:"id"` // 验证码 ID
|
||||
B64 string `json:"b64"` // base64 验证码
|
||||
Code string `json:"-"` // 验证码内容
|
||||
ExpiresAt time.Time `json:"-"` // 过期时间
|
||||
}
|
||||
type LoginLimiter struct {
|
||||
mu sync.RWMutex
|
||||
failCount map[string]int // 记录每个 IP 的失败次数
|
||||
timestamp map[string]time.Time // 记录每个 IP 的最后失败时间
|
||||
captchas map[string]Captcha // 每个 IP 的验证码
|
||||
threshold int // 失败阈值
|
||||
expiry time.Duration // 失败记录过期时间
|
||||
}
|
||||
|
||||
func NewLoginLimiter(threshold int, expiry time.Duration) *LoginLimiter {
|
||||
return &LoginLimiter{
|
||||
failCount: make(map[string]int),
|
||||
timestamp: make(map[string]time.Time),
|
||||
captchas: make(map[string]Captcha),
|
||||
threshold: threshold,
|
||||
expiry: expiry,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordFailure 记录登录失败
|
||||
func (l *LoginLimiter) RecordFailure(ip string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// 如果该 IP 的记录已经过期,重置计数
|
||||
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) > l.expiry {
|
||||
l.failCount[ip] = 0
|
||||
}
|
||||
|
||||
// 更新失败次数和时间戳
|
||||
l.failCount[ip]++
|
||||
l.timestamp[ip] = time.Now()
|
||||
}
|
||||
|
||||
// NeedsCaptcha 检查是否需要验证码
|
||||
func (l *LoginLimiter) NeedsCaptcha(ip string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
// 检查记录是否存在且未过期
|
||||
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) <= l.expiry {
|
||||
return l.failCount[ip] >= l.threshold
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateCaptcha 为指定 IP 生成验证码
|
||||
func (l *LoginLimiter) GenerateCaptcha(ip string) Captcha {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
capd := base64Captcha.NewDriverString(50, 150, 5, 10, 4, "1234567890abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
|
||||
b64cap := base64Captcha.NewCaptcha(capd, base64Captcha.DefaultMemStore)
|
||||
id, b64s, answer, err := b64cap.Generate()
|
||||
if err != nil {
|
||||
global.Logger.Error("Generate captcha failed: " + err.Error())
|
||||
return Captcha{}
|
||||
}
|
||||
// 保存验证码到对应 IP
|
||||
l.captchas[ip] = Captcha{
|
||||
Id: id,
|
||||
B64: b64s,
|
||||
Code: answer,
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
return l.captchas[ip]
|
||||
}
|
||||
|
||||
// VerifyCaptcha 验证指定 IP 的验证码
|
||||
func (l *LoginLimiter) VerifyCaptcha(ip, code string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
// 检查验证码是否存在且未过期
|
||||
if captcha, exists := l.captchas[ip]; exists && time.Now().Before(captcha.ExpiresAt) {
|
||||
return captcha.Code == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveCaptcha 移除指定 IP 的验证码
|
||||
func (l *LoginLimiter) RemoveCaptcha(ip string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
delete(l.captchas, ip)
|
||||
}
|
||||
|
||||
// CleanupExpired 清理过期的记录
|
||||
func (l *LoginLimiter) CleanupExpired() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for ip, lastTime := range l.timestamp {
|
||||
if now.Sub(lastTime) > l.expiry {
|
||||
delete(l.failCount, ip)
|
||||
delete(l.timestamp, ip)
|
||||
delete(l.captchas, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoginLimiter) RemoveRecord(ip string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
delete(l.failCount, ip)
|
||||
delete(l.timestamp, ip)
|
||||
delete(l.captchas, ip)
|
||||
}
|
||||
|
||||
var loginLimiter = NewLoginLimiter(3, 5*time.Minute)
|
||||
|
||||
// Login 登录
|
||||
// @Tags 登录
|
||||
// @Summary 登录
|
||||
@@ -156,10 +32,16 @@ func (ct *Login) Login(c *gin.Context) {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "PwdLoginDisabled"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查登录限制
|
||||
loginLimiter := global.LoginLimiter
|
||||
clientIp := c.ClientIP()
|
||||
_, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
|
||||
|
||||
f := &admin.Login{}
|
||||
err := c.ShouldBindJSON(f)
|
||||
clientIp := c.ClientIP()
|
||||
if err != nil {
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||
return
|
||||
@@ -167,14 +49,15 @@ func (ct *Login) Login(c *gin.Context) {
|
||||
|
||||
errList := global.Validator.ValidStruct(c, f)
|
||||
if len(errList) > 0 {
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
|
||||
response.Fail(c, 101, errList[0])
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要验证码
|
||||
if loginLimiter.NeedsCaptcha(clientIp) {
|
||||
if f.Captcha == "" || !loginLimiter.VerifyCaptcha(clientIp, f.Captcha) {
|
||||
if needCaptcha {
|
||||
if f.CaptchaId == "" || f.Captcha == "" || !loginLimiter.VerifyCaptcha(f.CaptchaId, f.Captcha) {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError"))
|
||||
return
|
||||
}
|
||||
@@ -184,17 +67,19 @@ func (ct *Login) Login(c *gin.Context) {
|
||||
|
||||
if u.Id == 0 {
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
|
||||
loginLimiter.RecordFailure(clientIp)
|
||||
if loginLimiter.NeedsCaptcha(clientIp) {
|
||||
loginLimiter.RemoveCaptcha(clientIp)
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
if _, needCaptcha = loginLimiter.CheckSecurityStatus(clientIp); needCaptcha {
|
||||
response.Fail(c, 110, response.TranslateMsg(c, "UsernameOrPasswordError"))
|
||||
} else {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
|
||||
}
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
|
||||
return
|
||||
}
|
||||
|
||||
if !service.AllService.UserService.CheckUserEnable(u) {
|
||||
if loginLimiter.NeedsCaptcha(clientIp) {
|
||||
loginLimiter.RemoveCaptcha(clientIp)
|
||||
if needCaptcha {
|
||||
response.Fail(c, 110, response.TranslateMsg(c, "UserDisabled"))
|
||||
return
|
||||
}
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "UserDisabled"))
|
||||
return
|
||||
@@ -209,23 +94,37 @@ func (ct *Login) Login(c *gin.Context) {
|
||||
Platform: f.Platform,
|
||||
})
|
||||
|
||||
// 成功后清除记录
|
||||
loginLimiter.RemoveRecord(clientIp)
|
||||
|
||||
// 清理过期记录
|
||||
go loginLimiter.CleanupExpired()
|
||||
|
||||
// 登录成功,清除登录限制
|
||||
loginLimiter.RemoveAttempts(clientIp)
|
||||
responseLoginSuccess(c, u, ut.Token)
|
||||
}
|
||||
func (ct *Login) Captcha(c *gin.Context) {
|
||||
loginLimiter := global.LoginLimiter
|
||||
clientIp := c.ClientIP()
|
||||
if !loginLimiter.NeedsCaptcha(clientIp) {
|
||||
banned, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
|
||||
if banned {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "LoginBanned"))
|
||||
return
|
||||
}
|
||||
if !needCaptcha {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "NoCaptchaRequired"))
|
||||
return
|
||||
}
|
||||
captcha := loginLimiter.GenerateCaptcha(clientIp)
|
||||
err, captcha := loginLimiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError")+err.Error())
|
||||
return
|
||||
}
|
||||
err, b64 := loginLimiter.DrawCaptcha(captcha.Content)
|
||||
if err != nil {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError")+err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{
|
||||
"captcha": captcha,
|
||||
"captcha": gin.H{
|
||||
"id": captcha.Id,
|
||||
"b64": b64,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -257,12 +156,18 @@ func (ct *Login) Logout(c *gin.Context) {
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /admin/login-options [post]
|
||||
func (ct *Login) LoginOptions(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
loginLimiter := global.LoginLimiter
|
||||
clientIp := c.ClientIP()
|
||||
banned, needCaptcha := loginLimiter.CheckSecurityStatus(clientIp)
|
||||
if banned {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "LoginBanned"))
|
||||
return
|
||||
}
|
||||
ops := service.AllService.OauthService.GetOauthProviders()
|
||||
response.Success(c, gin.H{
|
||||
"ops": ops,
|
||||
"register": global.Config.App.Register,
|
||||
"need_captcha": loginLimiter.NeedsCaptcha(ip),
|
||||
"need_captcha": needCaptcha,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -100,21 +100,21 @@ func (abcr *AddressBookCollectionRule) CheckForm(u *model.User, t *model.Address
|
||||
//check to_id
|
||||
if t.Type == model.ShareAddressBookRuleTypePersonal {
|
||||
if t.ToId == t.UserId {
|
||||
return "ParamsError", false
|
||||
return "CannotShareToSelf", false
|
||||
}
|
||||
tou := service.AllService.UserService.InfoById(t.ToId)
|
||||
if tou.Id == 0 {
|
||||
return "ItemNotFound", false
|
||||
}
|
||||
//非管理员不能分享给非本组织用户
|
||||
if tou.GroupId != u.GroupId {
|
||||
return "NoAccess", false
|
||||
}
|
||||
//if tou.GroupId != u.GroupId {
|
||||
// return "NoAccess", false
|
||||
//}
|
||||
} else if t.Type == model.ShareAddressBookRuleTypeGroup {
|
||||
//非管理员不能分享给其他组
|
||||
if t.ToId != u.GroupId {
|
||||
return "NoAccess", false
|
||||
}
|
||||
//if t.ToId != u.GroupId {
|
||||
// return "NoAccess", false
|
||||
//}
|
||||
|
||||
tog := service.AllService.GroupService.InfoById(t.ToId)
|
||||
if tog.Id == 0 {
|
||||
@@ -124,7 +124,7 @@ func (abcr *AddressBookCollectionRule) CheckForm(u *model.User, t *model.Address
|
||||
return "ParamsError", false
|
||||
}
|
||||
// 重复检查
|
||||
ex := service.AllService.AddressBookService.RulePersonalInfoByToIdAndCid(t.ToId, t.CollectionId)
|
||||
ex := service.AllService.AddressBookService.RuleInfoByToIdAndCid(t.Type, t.ToId, t.CollectionId)
|
||||
if t.Id == 0 && ex.Id > 0 {
|
||||
return "ItemExists", false
|
||||
}
|
||||
|
||||
@@ -108,6 +108,12 @@ func (ct *Peer) List(c *gin.Context) {
|
||||
if query.Uuids != "" {
|
||||
tx.Where("uuid in (?)", query.Uuids)
|
||||
}
|
||||
if query.Username != "" {
|
||||
tx.Where("username like ?", "%"+query.Username+"%")
|
||||
}
|
||||
if query.Ip != "" {
|
||||
tx.Where("last_online_ip like ?", "%"+query.Ip+"%")
|
||||
}
|
||||
})
|
||||
response.Success(c, res)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,16 @@ func (r *Rustdesk) SendCmd(c *gin.Context) {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
|
||||
return
|
||||
}
|
||||
res, err := service.AllService.ServerCmdService.SendCmd(rc.Target, rc.Cmd, rc.Option)
|
||||
|
||||
port := 0
|
||||
switch rc.Target {
|
||||
case model.ServerCmdTargetIdServer:
|
||||
port = global.Config.Admin.IdServerPort - 1
|
||||
case model.ServerCmdTargetRelayServer:
|
||||
port = global.Config.Admin.RelayServerPort
|
||||
}
|
||||
|
||||
res, err := service.AllService.ServerCmdService.SendCmd(port, rc.Cmd, rc.Option)
|
||||
if err != nil {
|
||||
response.Fail(c, 101, err.Error())
|
||||
return
|
||||
|
||||
@@ -296,32 +296,12 @@ func (ct *User) MyOauth(c *gin.Context) {
|
||||
|
||||
// groupUsers
|
||||
func (ct *User) GroupUsers(c *gin.Context) {
|
||||
q := &admin.GroupUsersQuery{}
|
||||
if err := c.ShouldBindJSON(q); err != nil {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||
return
|
||||
}
|
||||
u := service.AllService.UserService.CurUser(c)
|
||||
gid := u.GroupId
|
||||
uid := u.Id
|
||||
if service.AllService.UserService.IsAdmin(u) && q.UserId > 0 {
|
||||
nu := service.AllService.UserService.InfoById(q.UserId)
|
||||
gid = nu.GroupId
|
||||
uid = q.UserId
|
||||
}
|
||||
res := service.AllService.UserService.List(1, 999, func(tx *gorm.DB) {
|
||||
tx.Where("group_id = ?", gid)
|
||||
aG := service.AllService.GroupService.List(1, 999, nil)
|
||||
aU := service.AllService.UserService.List(1, 9999, nil)
|
||||
response.Success(c, gin.H{
|
||||
"groups": aG.Groups,
|
||||
"users": aU.Users,
|
||||
})
|
||||
var data []*adResp.GroupUsersPayload
|
||||
for _, _u := range res.Users {
|
||||
gup := &adResp.GroupUsersPayload{}
|
||||
gup.FromUser(_u)
|
||||
if _u.Id == uid {
|
||||
gup.Status = 0
|
||||
}
|
||||
data = append(data, gup)
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
// Register
|
||||
@@ -340,11 +320,22 @@ func (ct *User) Register(c *gin.Context) {
|
||||
response.Fail(c, 101, errList[0])
|
||||
return
|
||||
}
|
||||
u := service.AllService.UserService.Register(f.Username, f.Email, f.Password)
|
||||
regStatus := model.StatusCode(global.Config.App.RegisterStatus)
|
||||
// 注册状态可能未配置,默认启用
|
||||
if regStatus != model.COMMON_STATUS_DISABLED && regStatus != model.COMMON_STATUS_ENABLE {
|
||||
regStatus = model.COMMON_STATUS_ENABLE
|
||||
}
|
||||
|
||||
u := service.AllService.UserService.Register(f.Username, f.Email, f.Password, regStatus)
|
||||
if u == nil || u.Id == 0 {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed"))
|
||||
return
|
||||
}
|
||||
if regStatus == model.COMMON_STATUS_DISABLED {
|
||||
// 需要管理员审核
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "RegisterSuccessWaitAdminConfirm"))
|
||||
return
|
||||
}
|
||||
// 注册成功后自动登录
|
||||
ut := service.AllService.UserService.Login(u, &model.LoginLog{
|
||||
UserId: u.Id,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -56,7 +55,7 @@ func (i *Index) Heartbeat(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
//如果在40s以内则不更新
|
||||
if time.Now().Unix()-peer.LastOnlineTime > 40 {
|
||||
if time.Now().Unix()-peer.LastOnlineTime >= 30 {
|
||||
upp := &model.Peer{RowId: peer.RowId, LastOnlineTime: time.Now().Unix(), LastOnlineIp: c.ClientIP()}
|
||||
service.AllService.PeerService.Update(upp)
|
||||
}
|
||||
@@ -74,13 +73,9 @@ func (i *Index) Heartbeat(c *gin.Context) {
|
||||
// @Router /version [get]
|
||||
func (i *Index) Version(c *gin.Context) {
|
||||
//读取resources/version文件
|
||||
v, err := os.ReadFile("resources/version")
|
||||
if err != nil {
|
||||
response.Fail(c, 101, err.Error())
|
||||
return
|
||||
}
|
||||
v := service.AllService.AppService.GetAppVersion()
|
||||
response.Success(
|
||||
c,
|
||||
string(v),
|
||||
v,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,16 @@ func (l *Login) Login(c *gin.Context) {
|
||||
response.Error(c, response.TranslateMsg(c, "PwdLoginDisabled"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查登录限制
|
||||
loginLimiter := global.LoginLimiter
|
||||
clientIp := c.ClientIP()
|
||||
|
||||
f := &api.LoginForm{}
|
||||
err := c.ShouldBindJSON(f)
|
||||
//fmt.Println(f)
|
||||
if err != nil {
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
|
||||
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||
return
|
||||
@@ -42,6 +48,7 @@ func (l *Login) Login(c *gin.Context) {
|
||||
|
||||
errList := global.Validator.ValidStruct(c, f)
|
||||
if len(errList) > 0 {
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
|
||||
response.Error(c, errList[0])
|
||||
return
|
||||
@@ -50,6 +57,7 @@ func (l *Login) Login(c *gin.Context) {
|
||||
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
|
||||
|
||||
if u.Id == 0 {
|
||||
loginLimiter.RecordFailedAttempt(clientIp)
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), c.ClientIP()))
|
||||
response.Error(c, response.TranslateMsg(c, "UsernameOrPasswordError"))
|
||||
return
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
apiResp "github.com/lejianwen/rustdesk-api/v2/http/response/api"
|
||||
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||
"github.com/lejianwen/rustdesk-api/v2/utils"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -145,7 +147,8 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
state := c.Query("state")
|
||||
if state == "" {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateParamMsg(c, "ParamIsEmpty", "state"),
|
||||
"message": "ParamIsEmpty",
|
||||
"sub_message": "state",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -155,7 +158,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
oauthCache := oauthService.GetOauthCache(cacheKey)
|
||||
if oauthCache == nil {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "OauthExpired"),
|
||||
"message": "OauthExpired",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -169,7 +172,8 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
err, oauthUser := oauthService.Callback(code, verifier, op, nonce)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "OauthFailed") + response.TranslateMsg(c, err.Error()),
|
||||
"message": "OauthFailed",
|
||||
"sub_message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -182,7 +186,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
utr := oauthService.UserThirdInfo(op, openid)
|
||||
if utr.UserId > 0 {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "OauthHasBindOtherUser"),
|
||||
"message": "OauthHasBindOtherUser",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -190,7 +194,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
user = service.AllService.UserService.InfoById(userId)
|
||||
if user == nil {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "ItemNotFound"),
|
||||
"message": "ItemNotFound",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -198,12 +202,12 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
err := oauthService.BindOauthUser(userId, oauthUser, op)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "BindFail"),
|
||||
"message": "BindFail",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "oauth_success.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "BindSuccess"),
|
||||
"message": "BindSuccess",
|
||||
})
|
||||
return
|
||||
|
||||
@@ -211,7 +215,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
//登录
|
||||
if userId != 0 {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "OauthHasBeenSuccess"),
|
||||
"message": "OauthHasBeenSuccess",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -230,7 +234,7 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
err, user = service.AllService.UserService.RegisterByOauth(oauthUser, op)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, err.Error()),
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -252,14 +256,50 @@ func (o *Oauth) OauthCallback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "oauth_success.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "OauthSuccess"),
|
||||
"message": "OauthSuccess",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
c.HTML(http.StatusOK, "oauth_fail.html", gin.H{
|
||||
"message": response.TranslateMsg(c, "ParamsError"),
|
||||
"message": "ParamsError",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MessageParams struct {
|
||||
Lang string `json:"lang" form:"lang"`
|
||||
Title string `json:"title" form:"title"`
|
||||
Msg string `json:"msg" form:"msg"`
|
||||
}
|
||||
|
||||
func (o *Oauth) Message(c *gin.Context) {
|
||||
mp := &MessageParams{}
|
||||
if err := c.ShouldBindQuery(mp); err != nil {
|
||||
return
|
||||
}
|
||||
localizer := global.Localizer(mp.Lang)
|
||||
res := ""
|
||||
if mp.Title != "" {
|
||||
title, err := localizer.LocalizeMessage(&i18n.Message{
|
||||
ID: mp.Title,
|
||||
})
|
||||
if err == nil {
|
||||
res = utils.StringConcat(";title='", title, "';")
|
||||
}
|
||||
|
||||
}
|
||||
if mp.Msg != "" {
|
||||
msg, err := localizer.LocalizeMessage(&i18n.Message{
|
||||
ID: mp.Msg,
|
||||
})
|
||||
if err == nil {
|
||||
res = utils.StringConcat(res, "msg = '", msg, "';")
|
||||
}
|
||||
}
|
||||
|
||||
//返回js内容
|
||||
c.Header("Content-Type", "application/javascript")
|
||||
c.String(http.StatusOK, res)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
requstform "github.com/lejianwen/rustdesk-api/v2/http/request/api"
|
||||
@@ -13,7 +14,7 @@ type Peer struct {
|
||||
}
|
||||
|
||||
// SysInfo
|
||||
// @Tags 地址
|
||||
// @Tags System
|
||||
// @Summary 提交系统信息
|
||||
// @Description 提交系统信息
|
||||
// @Accept json
|
||||
@@ -30,7 +31,7 @@ func (p *Peer) SysInfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
fpe := f.ToPeer()
|
||||
pe := service.AllService.PeerService.FindById(f.Id)
|
||||
pe := service.AllService.PeerService.FindByUuid(f.Uuid)
|
||||
if pe.RowId == 0 {
|
||||
pe = f.ToPeer()
|
||||
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
|
||||
@@ -56,3 +57,20 @@ func (p *Peer) SysInfo(c *gin.Context) {
|
||||
//直接响应文本
|
||||
c.String(http.StatusOK, "SYSINFO_UPDATED")
|
||||
}
|
||||
|
||||
// SysInfoVer
|
||||
// @Tags System
|
||||
// @Summary 获取系统版本信息
|
||||
// @Description 获取系统版本信息
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string ""
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /sysinfo_ver [post]
|
||||
func (p *Peer) SysInfoVer(c *gin.Context) {
|
||||
//读取resources/version文件
|
||||
v := service.AllService.AppService.GetAppVersion()
|
||||
// 加上启动时间,方便client上传信息
|
||||
v = fmt.Sprintf("%s\n%s", v, service.AllService.AppService.GetStartTime())
|
||||
c.String(http.StatusOK, v)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func ApiInit() {
|
||||
g.NoRoute(func(c *gin.Context) {
|
||||
c.String(http.StatusNotFound, "404 not found")
|
||||
})
|
||||
g.Use(middleware.Logger(), gin.Recovery())
|
||||
g.Use(middleware.Logger(), middleware.Limiter(), gin.Recovery())
|
||||
router.WebInit(g)
|
||||
router.Init(g)
|
||||
router.ApiInit(g)
|
||||
|
||||
22
http/middleware/limiter.go
Normal file
22
http/middleware/limiter.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||
"github.com/lejianwen/rustdesk-api/v2/http/response"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Limiter() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
loginLimiter := global.LoginLimiter
|
||||
clientIp := c.ClientIP()
|
||||
banned, _ := loginLimiter.CheckSecurityStatus(clientIp)
|
||||
if banned {
|
||||
response.Fail(c, http.StatusLocked, response.TranslateMsg(c, "Banned"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package admin
|
||||
|
||||
type Login struct {
|
||||
Username string `json:"username" validate:"required" label:"用户名"`
|
||||
Password string `json:"password,omitempty" validate:"required" label:"密码"`
|
||||
Platform string `json:"platform" label:"平台"`
|
||||
Captcha string `json:"captcha,omitempty" label:"验证码"`
|
||||
Username string `json:"username" validate:"required" label:"用户名"`
|
||||
Password string `json:"password,omitempty" validate:"required" label:"密码"`
|
||||
Platform string `json:"platform" label:"平台"`
|
||||
Captcha string `json:"captcha,omitempty" label:"验证码"`
|
||||
CaptchaId string `json:"captcha_id,omitempty"`
|
||||
}
|
||||
|
||||
type LoginLogQuery struct {
|
||||
|
||||
@@ -41,6 +41,8 @@ type PeerQuery struct {
|
||||
Id string `json:"id" form:"id"`
|
||||
Hostname string `json:"hostname" form:"hostname"`
|
||||
Uuids string `json:"uuids" form:"uuids"`
|
||||
Ip string `json:"ip" form:"ip"`
|
||||
Username string `json:"username" form:"username"`
|
||||
}
|
||||
|
||||
type SimpleDataQuery struct {
|
||||
|
||||
@@ -40,14 +40,14 @@ type LoginForm struct {
|
||||
|
||||
type UserListQuery struct {
|
||||
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
|
||||
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
|
||||
PageSize uint `json:"pageSize" form:"pageSize" validate:"required" label:"每页数量"`
|
||||
Status int `json:"status" form:"status" label:"状态"`
|
||||
Accessible string `json:"accessible" form:"accessible"`
|
||||
}
|
||||
|
||||
type PeerListQuery struct {
|
||||
Page uint `json:"page" form:"page" validate:"required" label:"页码"`
|
||||
PageSize uint `json:"page_size" form:"page_size" validate:"required" label:"每页数量"`
|
||||
PageSize uint `json:"pageSize" form:"pageSize" validate:"required" label:"每页数量"`
|
||||
Status int `json:"status" form:"status" label:"状态"`
|
||||
Accessible string `json:"accessible" form:"accessible"`
|
||||
}
|
||||
|
||||
@@ -22,15 +22,3 @@ type UserOauthItem struct {
|
||||
Op string `json:"op"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type GroupUsersPayload struct {
|
||||
Id uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func (g *GroupUsersPayload) FromUser(user *model.User) {
|
||||
g.Id = user.Id
|
||||
g.Username = user.Username
|
||||
g.Status = 1
|
||||
}
|
||||
|
||||
@@ -48,11 +48,13 @@ func ApiInit(g *gin.Engine) {
|
||||
//api/oauth/callback
|
||||
frg.GET("/oauth/callback", o.OauthCallback)
|
||||
frg.GET("/oauth/login", o.OauthCallback)
|
||||
frg.GET("/oauth/msg", o.Message)
|
||||
}
|
||||
{
|
||||
pe := &api.Peer{}
|
||||
//提交系统信息
|
||||
frg.POST("/sysinfo", pe.SysInfo)
|
||||
frg.POST("/sysinfo_ver", pe.SysInfoVer)
|
||||
}
|
||||
|
||||
if global.Config.App.WebClient == 1 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -10,14 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type MysqlConfig struct {
|
||||
Dns string
|
||||
Dsn string
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
}
|
||||
|
||||
func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
|
||||
func NewMysql(mysqlConf *MysqlConfig, logwriter logger.Writer) *gorm.DB {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: mysqlConf.Dns, // DSN data source name
|
||||
DSN: mysqlConf.Dsn, // DSN data source name
|
||||
DefaultStringSize: 256, // string 类型字段的默认长度
|
||||
//DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
|
||||
//DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
|
||||
@@ -26,7 +25,7 @@ func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
|
||||
}), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: logger.New(
|
||||
global.Logger, // io writer
|
||||
logwriter, // io writer
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL threshold
|
||||
LogLevel: logger.Warn, // Log level
|
||||
|
||||
45
lib/orm/postgresql.go
Normal file
45
lib/orm/postgresql.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PostgresqlConfig struct {
|
||||
Dsn string
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
}
|
||||
|
||||
func NewPostgresql(conf *PostgresqlConfig, logwriter logger.Writer) *gorm.DB {
|
||||
db, err := gorm.Open(postgres.Open(conf.Dsn), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: logger.New(
|
||||
logwriter, // io writer
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL threshold
|
||||
LogLevel: logger.Warn, // Log level
|
||||
//IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||
ParameterizedQueries: true, // Don't include params in the SQL log
|
||||
Colorful: true,
|
||||
},
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
sqlDB, err2 := db.DB()
|
||||
if err2 != nil {
|
||||
fmt.Println(err2)
|
||||
}
|
||||
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
|
||||
sqlDB.SetMaxIdleConns(conf.MaxIdleConns)
|
||||
|
||||
// SetMaxOpenConns 设置打开数据库连接的最大数量。
|
||||
sqlDB.SetMaxOpenConns(conf.MaxOpenConns)
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -14,11 +13,11 @@ type SqliteConfig struct {
|
||||
MaxOpenConns int
|
||||
}
|
||||
|
||||
func NewSqlite(sqliteConf *SqliteConfig) *gorm.DB {
|
||||
func NewSqlite(sqliteConf *SqliteConfig, logwriter logger.Writer) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("./data/rustdeskapi.db"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: logger.New(
|
||||
global.Logger, // io writer
|
||||
logwriter, // io writer
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL threshold
|
||||
LogLevel: logger.Warn, // Log level
|
||||
|
||||
@@ -14,6 +14,7 @@ const (
|
||||
OauthTypeGoogle string = "google"
|
||||
OauthTypeOidc string = "oidc"
|
||||
OauthTypeWebauth string = "webauth"
|
||||
OauthTypeLinuxdo string = "linuxdo"
|
||||
PKCEMethodS256 string = "S256"
|
||||
PKCEMethodPlain string = "plain"
|
||||
)
|
||||
@@ -21,7 +22,7 @@ const (
|
||||
// Validate the oauth type
|
||||
func ValidateOauthType(oauthType string) error {
|
||||
switch oauthType {
|
||||
case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth:
|
||||
case OauthTypeGithub, OauthTypeGoogle, OauthTypeOidc, OauthTypeWebauth, OauthTypeLinuxdo:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid Oauth type")
|
||||
@@ -30,6 +31,7 @@ func ValidateOauthType(oauthType string) error {
|
||||
|
||||
const (
|
||||
UserEndpointGithub string = "https://api.github.com/user"
|
||||
UserEndpointLinuxdo string = "https://connect.linux.do/api/user"
|
||||
IssuerGoogle string = "https://accounts.google.com"
|
||||
)
|
||||
|
||||
@@ -60,6 +62,8 @@ func (oa *Oauth) FormatOauthInfo() error {
|
||||
oa.Op = OauthTypeGithub
|
||||
case OauthTypeGoogle:
|
||||
oa.Op = OauthTypeGoogle
|
||||
case OauthTypeLinuxdo:
|
||||
oa.Op = OauthTypeLinuxdo
|
||||
}
|
||||
// check if the op is empty, set the default value
|
||||
op := strings.TrimSpace(oa.Op)
|
||||
@@ -152,6 +156,24 @@ func (gu *GithubUser) ToOauthUser() *OauthUser {
|
||||
}
|
||||
}
|
||||
|
||||
type LinuxdoUser struct {
|
||||
OauthUserBase
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
func (lu *LinuxdoUser) ToOauthUser() *OauthUser {
|
||||
return &OauthUser{
|
||||
OpenId: strconv.Itoa(lu.Id),
|
||||
Name: lu.Name,
|
||||
Username: strings.ToLower(lu.Username),
|
||||
Email: lu.Email,
|
||||
VerifiedEmail: true, // linux.do 用户邮箱默认已验证
|
||||
Picture: lu.Avatar,
|
||||
}
|
||||
}
|
||||
|
||||
type OauthList struct {
|
||||
Oauths []*Oauth `json:"list"`
|
||||
Pagination
|
||||
|
||||
@@ -138,3 +138,18 @@ other = "Captcha error."
|
||||
description = "Password login disabled."
|
||||
one = "Password login disabled."
|
||||
other = "Password login disabled."
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "Cannot share to self."
|
||||
other = "Cannot share to self."
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "Banned."
|
||||
other = "Banned."
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success, wait admin confirm."
|
||||
one = "Register success, wait admin confirm."
|
||||
other = "Register success, wait admin confirm."
|
||||
@@ -147,3 +147,18 @@ other = "Error de captcha."
|
||||
description = "Password login disabled."
|
||||
one = "Inicio de sesión con contraseña deshabilitado."
|
||||
other = "Inicio de sesión con contraseña deshabilitado."
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "No se puede compartir con uno mismo."
|
||||
other = "No se puede compartir con uno mismo."
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "Prohibido."
|
||||
other = "Prohibido."
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success, wait admin confirm."
|
||||
one = "Registro exitoso, espere la confirmación del administrador."
|
||||
other = "Registro exitoso, espere la confirmación del administrador."
|
||||
@@ -147,3 +147,18 @@ other = "Erreur de captcha."
|
||||
description = "Password login disabled."
|
||||
one = "Connexion par mot de passe désactivée."
|
||||
other = "Connexion par mot de passe désactivée."
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "Impossible de partager avec soi-même."
|
||||
other = "Impossible de partager avec soi-même."
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "Banni."
|
||||
other = "Banni."
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success wait admin confirm."
|
||||
one = "Inscription réussie, veuillez attendre la confirmation de l'administrateur."
|
||||
other = "Inscription réussie, veuillez attendre la confirmation de l'administrateur."
|
||||
@@ -141,3 +141,18 @@ other = "Captcha 오류."
|
||||
description = "Password login disabled."
|
||||
one = "비밀번호 로그인이 비활성화되었습니다."
|
||||
other = "비밀번호 로그인이 비활성화되었습니다."
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "자기 자신에게 공유할 수 없습니다."
|
||||
other = "자기 자신에게 공유할 수 없습니다."
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "금지됨."
|
||||
other = "금지됨."
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success wait admin confirm."
|
||||
one = "가입 성공, 관리자 확인 대기 중."
|
||||
other = "가입 성공, 관리자 확인 대기 중."
|
||||
@@ -147,3 +147,18 @@ other = "Ошибка капчи."
|
||||
description = "Password login disabled."
|
||||
one = "Вход по паролю отключен."
|
||||
other = "Вход по паролю отключен."
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "Нельзя поделиться с собой."
|
||||
other = "Нельзя поделиться с собой."
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "Заблокировано."
|
||||
other = "Заблокировано."
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success wait admin confirm."
|
||||
one = "Регистрация прошла успешно, ожидайте подтверждения администратора."
|
||||
other = "Регистрация прошла успешно, ожидайте подтверждения администратора."
|
||||
@@ -140,3 +140,18 @@ other = "验证码错误。"
|
||||
description = "Password login disabled."
|
||||
one = "密码登录已禁用。"
|
||||
other = "密码登录已禁用。"
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "不能共享给自己。"
|
||||
other = "不能共享给自己。"
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "已被封禁。"
|
||||
other = "已被封禁。"
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success, wait for admin confirm."
|
||||
one = "注册成功,请等待管理员审核。"
|
||||
other = "注册成功,请等待管理员审核。"
|
||||
@@ -140,3 +140,18 @@ other = "驗證碼錯誤。"
|
||||
description = "Password login disabled."
|
||||
one = "密碼登錄已禁用。"
|
||||
other = "密碼登錄已禁用。"
|
||||
|
||||
[CannotShareToSelf]
|
||||
description = "Cannot share to self."
|
||||
one = "無法共享給自己。"
|
||||
other = "無法共享給自己。"
|
||||
|
||||
[Banned]
|
||||
description = "Banned."
|
||||
one = "禁止使用。"
|
||||
other = "禁止使用。"
|
||||
|
||||
[RegisterSuccessWaitAdminConfirm]
|
||||
description = "Register success wait admin confirm."
|
||||
one = "註冊成功,請等待管理員確認。"
|
||||
other = "註冊成功,請等待管理員確認。"
|
||||
0
resources/public/upload/.gitkeep
Normal file
0
resources/public/upload/.gitkeep
Normal file
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>授权失败 - RustDesk API</title>
|
||||
<title>OauthFailed - RustDesk API</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
@@ -57,17 +57,25 @@
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css">
|
||||
<script>
|
||||
var lang = navigator.language || navigator.userLanguage || 'zh-CN';
|
||||
var title = 'OauthFailed'
|
||||
var msg = '{{.message}}'
|
||||
var btn = 'Close'
|
||||
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthFailed"><\/script>');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="success-container">
|
||||
<i class="fas fa-triangle-exclamation checkmark"></i>
|
||||
<h1>授权失败!</h1>
|
||||
<p>{{.message}}</p>
|
||||
<a href="javascript:window.close()" class="return-link">关闭页面</a>
|
||||
<i class="fas fa-triangle-exclamation checkmark"></i>
|
||||
<h1 id="h1"></h1>
|
||||
<p id="msg"></p>
|
||||
<a href="javascript:window.close()" class="return-link" id="btn">Close</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
document.title = title + ' - RustDesk API';
|
||||
document.getElementById('h1').innerText = title;
|
||||
document.getElementById('msg').innerText = msg;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>授权成功 - RustDesk API</title>
|
||||
<title>OauthSuccess - RustDesk API</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
@@ -56,18 +56,27 @@
|
||||
background-color: #45a049;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var lang = navigator.language || navigator.userLanguage || 'zh-CN';
|
||||
var title = 'OauthSuccess'
|
||||
var msg = '{{.message}}'
|
||||
var btn = 'Close'
|
||||
document.writeln('<script src="/api/oauth/msg?lang=' + lang + '&msg=' + msg + '&title=OauthSuccess"><\/script>');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="success-container">
|
||||
<i class="fas fa-check-circle checkmark"></i>
|
||||
<h1>授权成功!</h1>
|
||||
<p>您已成功授权访问您的账户。</p>
|
||||
<p>现在可以关闭本页面或返回应用继续操作。</p>
|
||||
<a href="javascript:window.close()" class="return-link">关闭页面</a>
|
||||
<h1 id="h1"></h1>
|
||||
<!-- <p>您已成功授权访问您的账户。</p>-->
|
||||
<!-- <p>现在可以关闭本页面或返回应用继续操作。</p>-->
|
||||
<a href="javascript:window.close()" class="return-link">Close</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
document.title = title + ' - RustDesk API';
|
||||
document.getElementById('h1').innerText = title;
|
||||
document.getElementById('msg').innerText = msg;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
resources/web2/assets/FontManifest.json
vendored
16
resources/web2/assets/FontManifest.json
vendored
@@ -38,5 +38,21 @@
|
||||
"asset": "assets/address_book.ttf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"family": "DeviceGroup",
|
||||
"fonts": [
|
||||
{
|
||||
"asset": "assets/device_group.ttf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"family": "More",
|
||||
"fonts": [
|
||||
{
|
||||
"asset": "assets/more.ttf"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
BIN
resources/web2/assets/assets/device_group.ttf
vendored
Normal file
BIN
resources/web2/assets/assets/device_group.ttf
vendored
Normal file
Binary file not shown.
BIN
resources/web2/assets/assets/more.ttf
vendored
Normal file
BIN
resources/web2/assets/assets/more.ttf
vendored
Normal file
Binary file not shown.
Binary file not shown.
513
resources/web2/index.html
vendored
513
resources/web2/index.html
vendored
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
@@ -16,195 +16,196 @@
|
||||
-->
|
||||
<base href="/webclient2/" />
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||
<meta name="description" content="Remote Desktop." />
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible"/>
|
||||
<meta name="description" content="Remote Desktop."/>
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="RustDesk" />
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png?v=1a7ad736" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
|
||||
<meta name="apple-mobile-web-app-title" content="RustDesk"/>
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png?v=1a7ad736"/>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg?v=8fcccd9a" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg?v=8fcccd9a"/>
|
||||
|
||||
<title>RustDesk</title>
|
||||
<script src="/webclient-config/index.js"></script>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<script type="module" crossorigin src="js/dist/index.js?v=cabfd933"></script>
|
||||
<link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e" />
|
||||
<link rel="manifest" href="manifest.json"/>
|
||||
<script type="module" crossorigin src="js/dist/index.js?v=ddbe54f1"></script>
|
||||
<link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e"/>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#root {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loading-sub-title {
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.page-loading-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 26px;
|
||||
}
|
||||
.ant-spin {
|
||||
position: absolute;
|
||||
display: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s
|
||||
cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
-webkit-font-feature-settings: "tnum";
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.ant-spin-spinning {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ant-spin-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
-webkit-transform: scale(0.75);
|
||||
-ms-transform: scale(0.75);
|
||||
transform: scale(0.75);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
-ms-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
opacity: 0.3;
|
||||
-webkit-animation: antspinmove 1s infinite linear alternate;
|
||||
animation: antSpinMove 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-spin {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: antrotate 1.2s infinite linear;
|
||||
animation: antRotate 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.ant-spin-blur {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
#root {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
.loading-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
.loading-sub-title {
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
.page-loading-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.ant-spin {
|
||||
position: absolute;
|
||||
display: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
-webkit-font-feature-settings: "tnum";
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.ant-spin-spinning {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ant-spin-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
-webkit-transform: scale(0.75);
|
||||
-ms-transform: scale(0.75);
|
||||
transform: scale(0.75);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
-ms-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
opacity: 0.3;
|
||||
-webkit-animation: antspinmove 1s infinite linear alternate;
|
||||
animation: antSpinMove 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-spin {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: antrotate 1.2s infinite linear;
|
||||
animation: antRotate 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.ant-spin-blur {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<div
|
||||
id="div-background"
|
||||
style="
|
||||
<body>
|
||||
<div id="root">
|
||||
<div
|
||||
id="div-background"
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -212,117 +213,119 @@
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
"
|
||||
>
|
||||
<img src="./favicon.svg?v=8fcccd9a" alt="logo" width="256" />
|
||||
>
|
||||
<img src="./favicon.svg?v=8fcccd9a" alt="logo" width="256"/>
|
||||
<div class="page-loading-warp">
|
||||
<div class="ant-spin ant-spin-lg ant-spin-spinning">
|
||||
<div class="ant-spin ant-spin-lg ant-spin-spinning">
|
||||
<span class="ant-spin-dot ant-spin-dot-spin">
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
<i class="ant-spin-dot-item"></i>
|
||||
<i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<img src="./favicon.svg?v=8fcccd9a" width="32" style="margin-right: 8px" />
|
||||
<span id="span-text">RustDesk Web Client V2 Preview</span>
|
||||
<img src="./favicon.svg?v=8fcccd9a" width="32" style="margin-right: 8px"/>
|
||||
<span id="span-text">RustDesk Web Client V2 Preview</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
</div>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
const myTheme = localStorage.getItem("wc-option:local:theme");
|
||||
const them = myTheme || systemTheme;
|
||||
const myTheme = localStorage.getItem("wc-option:local:theme");
|
||||
const them = myTheme || systemTheme;
|
||||
|
||||
const divBackground = document.querySelector("#div-background");
|
||||
if (divBackground) {
|
||||
const divBackground = document.querySelector("#div-background");
|
||||
if (divBackground) {
|
||||
divBackground.style.backgroundColor = them === "dark" ? "#000" : "#fff";
|
||||
}
|
||||
const spanConsole = document.querySelector("#span-text");
|
||||
if (spanConsole) {
|
||||
}
|
||||
const spanConsole = document.querySelector("#span-text");
|
||||
if (spanConsole) {
|
||||
spanConsole.style.color = them === "dark" ? "#fff" : "#000";
|
||||
}
|
||||
}
|
||||
|
||||
const serviceWorkerVersion = "3267265270";
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
const serviceWorkerVersion = "461457302";
|
||||
var scriptLoaded = false;
|
||||
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement("script");
|
||||
scriptTag.src = "main.dart.js?v=060a626e";
|
||||
scriptTag.src = "main.dart.js?v=6d16cb80";
|
||||
scriptTag.type = "application/javascript";
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
}
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
if ("serviceWorker" in navigator) {
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener("load", function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl =
|
||||
"flutter_service_worker.js?v=" + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener("statechange", () => {
|
||||
if (serviceWorker.state == "activated") {
|
||||
console.log("Installed new service worker.");
|
||||
loadMainDartJs();
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl =
|
||||
"flutter_service_worker.js?v=" + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener("statechange", () => {
|
||||
if (serviceWorker.state == "activated") {
|
||||
console.log("Installed new service worker.");
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!reg.active && (reg.installing || reg.waiting)) {
|
||||
// No active web worker and we have installed or are installing
|
||||
// one for the first time. Simply wait for it to activate.
|
||||
waitForActivation(reg.installing || reg.waiting);
|
||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||
// When the app updates the serviceWorkerVersion changes, so we
|
||||
// need to ask the service worker to update.
|
||||
console.log("New service worker available.");
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log("Loading app from service worker.");
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
"Failed to load app from service worker. Falling back to plain <script> tag."
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
if (!reg.active && (reg.installing || reg.waiting)) {
|
||||
// No active web worker and we have installed or are installing
|
||||
// one for the first time. Simply wait for it to activate.
|
||||
waitForActivation(reg.installing || reg.waiting);
|
||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||
// When the app updates the serviceWorkerVersion changes, so we
|
||||
// need to ask the service worker to update.
|
||||
console.log("New service worker available.");
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log("Loading app from service worker.");
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
"Failed to load app from service worker. Falling back to plain <script> tag."
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="libs/stream/ponyfill.min.js"></script>
|
||||
<script src="libs/stream/StreamSaver.min.js"></script>
|
||||
<script src="libs/firebase-app.js?8.10.1"></script>
|
||||
<script src="libs/firebase-analytics.js?8.10.1"></script>
|
||||
}
|
||||
</script>
|
||||
<script src="libs/stream/ponyfill.min.js"></script>
|
||||
<script src="libs/stream/StreamSaver.min.js"></script>
|
||||
<script src="libs/firebase-app.js?8.10.1"></script>
|
||||
<script src="libs/firebase-analytics.js?8.10.1"></script>
|
||||
|
||||
<script>
|
||||
// Your web app's Firebase configuration
|
||||
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||
const firebaseConfig = {
|
||||
<script>
|
||||
// Your web app's Firebase configuration
|
||||
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyCgehIZk1aFP0E7wZtYRRqrfvNiNAF39-A",
|
||||
authDomain: "rustdesk.firebaseapp.com",
|
||||
databaseURL: "https://rustdesk.firebaseio.com",
|
||||
@@ -331,11 +334,11 @@
|
||||
messagingSenderId: "768133699366",
|
||||
appId: "1:768133699366:web:d50faf0792cb208d7993e7",
|
||||
measurementId: "G-9PEH85N6ZQ",
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
firebase.analytics();
|
||||
</script>
|
||||
</body>
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
firebase.analytics();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6563
resources/web2/js/dist/index.js
vendored
6563
resources/web2/js/dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
7417
resources/web2/js/dist/lang.js
vendored
7417
resources/web2/js/dist/lang.js
vendored
File diff suppressed because it is too large
Load Diff
8
resources/web2/js/dist/ljw.js
vendored
8
resources/web2/js/dist/ljw.js
vendored
@@ -1,5 +1,11 @@
|
||||
window._gwen = {}
|
||||
window._gwen.kv = {}
|
||||
|
||||
//fix 语言
|
||||
if(!localStorage.getItem('wc-option:local:lang') && navigator.language){
|
||||
localStorage.setItem('wc-option:local:lang', navigator.language.toLowerCase())
|
||||
}
|
||||
|
||||
const storage_prefix = 'wc-'
|
||||
const apiserver = localStorage.getItem('wc-api-server')
|
||||
|
||||
@@ -46,7 +52,7 @@ if (share_token) {
|
||||
password: peer.tmppwd,
|
||||
}*/
|
||||
//修改location
|
||||
window.location.href = `/webclient2/#/${peer.info.id}?password=${peer.tmppwd}`
|
||||
window.location.href = `/webclient2/#/${peer.info.id}?password=${encodeURIComponent(peer.tmppwd)}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
163526
resources/web2/main.dart.js
vendored
163526
resources/web2/main.dart.js
vendored
File diff suppressed because one or more lines are too long
@@ -293,8 +293,11 @@ func (s *AddressBookService) RuleInfoById(u uint) *model.AddressBookCollectionRu
|
||||
return p
|
||||
}
|
||||
func (s *AddressBookService) RulePersonalInfoByToIdAndCid(toid, cid uint) *model.AddressBookCollectionRule {
|
||||
return s.RuleInfoByToIdAndCid(model.ShareAddressBookRuleTypePersonal, toid, cid)
|
||||
}
|
||||
func (s *AddressBookService) RuleInfoByToIdAndCid(t int, toid, cid uint) *model.AddressBookCollectionRule {
|
||||
p := &model.AddressBookCollectionRule{}
|
||||
DB.Where("type = ? and to_id = ? and collection_id = ?", model.ShareAddressBookRuleTypePersonal, toid, cid).First(p)
|
||||
DB.Where("type = ? and to_id = ? and collection_id = ?", t, toid, cid).First(p)
|
||||
return p
|
||||
}
|
||||
func (s *AddressBookService) CreateRule(t *model.AddressBookCollectionRule) error {
|
||||
|
||||
39
service/app.go
Normal file
39
service/app.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AppService struct {
|
||||
}
|
||||
|
||||
var version = ""
|
||||
var startTime = ""
|
||||
var once = &sync.Once{}
|
||||
|
||||
func (a *AppService) GetAppVersion() string {
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
once.Do(func() {
|
||||
v, err := os.ReadFile("resources/version")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
version = string(v)
|
||||
|
||||
})
|
||||
return version
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Initialize the AppService if needed
|
||||
startTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// GetStartTime
|
||||
func (a *AppService) GetStartTime() string {
|
||||
return startTime
|
||||
}
|
||||
33
service/app_test.go
Normal file
33
service/app_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGetAppVersion
|
||||
func TestGetAppVersion(t *testing.T) {
|
||||
s := &AppService{}
|
||||
v := s.GetAppVersion()
|
||||
// 打印结果
|
||||
t.Logf("App Version: %s", v)
|
||||
}
|
||||
|
||||
func TestMultipleGetAppVersion(t *testing.T) {
|
||||
s := &AppService{}
|
||||
//并发测试
|
||||
// 使用 WaitGroup 等待所有 goroutine 完成
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(10) // 启动 10 个 goroutine
|
||||
// 启动 10 个 goroutine
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
defer wg.Done() // 完成后减少计数
|
||||
v := s.GetAppVersion()
|
||||
// 打印结果
|
||||
t.Logf("App Version: %s", v)
|
||||
}()
|
||||
}
|
||||
// 等待所有 goroutine 完成
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -30,6 +30,7 @@ var (
|
||||
ErrLdapBindFailed = errors.New("LdapBindFailed")
|
||||
ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
|
||||
ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
|
||||
ErrLdapPasswordNotMatch = errors.New("PasswordNotMatch")
|
||||
)
|
||||
|
||||
// LdapService is responsible for LDAP authentication and user synchronization.
|
||||
@@ -119,7 +120,7 @@ func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error)
|
||||
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
|
||||
ldapConn, err := ls.connectAndBind(cfg, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
return ErrLdapPasswordNotMatch
|
||||
}
|
||||
defer ldapConn.Close()
|
||||
return nil
|
||||
@@ -136,6 +137,10 @@ func (ls *LdapService) Authenticate(username, password string) (*model.User, err
|
||||
return nil, ErrLdapUserDisabled
|
||||
}
|
||||
cfg := &Config.Ldap
|
||||
err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := ls.mapToLocalUser(cfg, ldapUser)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrLdapToLocalUserFailed, err)
|
||||
@@ -406,7 +411,7 @@ func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
|
||||
// Check "memberOf" directly
|
||||
if len(ldapUser.MemberOf) > 0 {
|
||||
for _, group := range ldapUser.MemberOf {
|
||||
if group == adminGroup {
|
||||
if strings.EqualFold(group, adminGroup) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,18 @@ func (os *OauthService) GithubProvider() *oidc.Provider {
|
||||
}).NewProvider(context.Background())
|
||||
}
|
||||
|
||||
func (os *OauthService) LinuxdoProvider() *oidc.Provider {
|
||||
return (&oidc.ProviderConfig{
|
||||
IssuerURL: "",
|
||||
AuthURL: "https://connect.linux.do/oauth2/authorize",
|
||||
TokenURL: "https://connect.linux.do/oauth2/token",
|
||||
DeviceAuthURL: "",
|
||||
UserInfoURL: model.UserEndpointLinuxdo,
|
||||
JWKSURL: "",
|
||||
Algorithms: nil,
|
||||
}).NewProvider(context.Background())
|
||||
}
|
||||
|
||||
// GetOauthConfig retrieves the OAuth2 configuration based on the provider name
|
||||
func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.Oauth, oauthConfig *oauth2.Config, provider *oidc.Provider) {
|
||||
//err, oauthInfo, oauthConfig = os.getOauthConfigGeneral(op)
|
||||
@@ -182,6 +194,10 @@ func (os *OauthService) GetOauthConfig(op string) (err error, oauthInfo *model.O
|
||||
oauthConfig.Endpoint = github.Endpoint
|
||||
oauthConfig.Scopes = []string{"read:user", "user:email"}
|
||||
provider = os.GithubProvider()
|
||||
case model.OauthTypeLinuxdo:
|
||||
provider = os.LinuxdoProvider()
|
||||
oauthConfig.Endpoint = provider.Endpoint()
|
||||
oauthConfig.Scopes = []string{"profile"}
|
||||
//case model.OauthTypeGoogle: //google单独出来,可以少一次FetchOidcEndpoint请求
|
||||
// oauthConfig.Endpoint = google.Endpoint
|
||||
// oauthConfig.Scopes = os.constructScopes(oauthInfo.Scopes)
|
||||
@@ -299,6 +315,16 @@ func (os *OauthService) githubCallback(oauthConfig *oauth2.Config, provider *oid
|
||||
return nil, user.ToOauthUser()
|
||||
}
|
||||
|
||||
// linuxdoCallback linux.do回调
|
||||
func (os *OauthService) linuxdoCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce string) (error, *model.OauthUser) {
|
||||
var user = &model.LinuxdoUser{}
|
||||
err, _ := os.callbackBase(oauthConfig, provider, code, verifier, nonce, user)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
return nil, user.ToOauthUser()
|
||||
}
|
||||
|
||||
// oidcCallback oidc回调, 通过code获取用户信息
|
||||
func (os *OauthService) oidcCallback(oauthConfig *oauth2.Config, provider *oidc.Provider, code, verifier, nonce string) (error, *model.OauthUser) {
|
||||
var user = &model.OidcUser{}
|
||||
@@ -319,6 +345,8 @@ func (os *OauthService) Callback(code, verifier, op, nonce string) (err error, o
|
||||
switch oauthType {
|
||||
case model.OauthTypeGithub:
|
||||
err, oauthUser = os.githubCallback(oauthConfig, provider, code, verifier, nonce)
|
||||
case model.OauthTypeLinuxdo:
|
||||
err, oauthUser = os.linuxdoCallback(oauthConfig, provider, code, verifier, nonce)
|
||||
case model.OauthTypeOidc, model.OauthTypeGoogle:
|
||||
err, oauthUser = os.oidcCallback(oauthConfig, provider, code, verifier, nonce)
|
||||
default:
|
||||
|
||||
@@ -126,7 +126,14 @@ func (ps *PeerService) GetUuidListByIDs(ids []uint) ([]string, error) {
|
||||
err := DB.Model(&model.Peer{}).
|
||||
Where("row_id in (?)", ids).
|
||||
Pluck("uuid", &uuids).Error
|
||||
return uuids, err
|
||||
//过滤uuids中的空字符串
|
||||
var newUuids []string
|
||||
for _, uuid := range uuids {
|
||||
if uuid != "" {
|
||||
newUuids = append(newUuids, uuid)
|
||||
}
|
||||
}
|
||||
return newUuids, err
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除, 同时也应该删除token
|
||||
|
||||
@@ -40,14 +40,7 @@ func (is *ServerCmdService) Create(u *model.ServerCmd) error {
|
||||
}
|
||||
|
||||
// SendCmd 发送命令
|
||||
func (is *ServerCmdService) SendCmd(target string, cmd string, arg string) (string, error) {
|
||||
port := 0
|
||||
switch target {
|
||||
case model.ServerCmdTargetIdServer:
|
||||
port = Config.Rustdesk.IdServerPort - 1
|
||||
case model.ServerCmdTargetRelayServer:
|
||||
port = Config.Rustdesk.RelayServerPort
|
||||
}
|
||||
func (is *ServerCmdService) SendCmd(port int, cmd string, arg string) (string, error) {
|
||||
//组装命令
|
||||
cmd = cmd + " " + arg
|
||||
res, err := is.SendSocketCmd("v6", port, cmd)
|
||||
|
||||
@@ -23,6 +23,7 @@ type Service struct {
|
||||
*ShareRecordService
|
||||
*ServerCmdService
|
||||
*LdapService
|
||||
*AppService
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
|
||||
@@ -412,12 +412,13 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
|
||||
}
|
||||
|
||||
// Register 注册, 如果用户名已存在则返回nil
|
||||
func (us *UserService) Register(username string, email string, password string) *model.User {
|
||||
func (us *UserService) Register(username string, email string, password string, status model.StatusCode) *model.User {
|
||||
u := &model.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
GroupId: 1,
|
||||
Status: status,
|
||||
}
|
||||
err := us.Create(u)
|
||||
if err != nil {
|
||||
|
||||
48
utils/captcha.go
Normal file
48
utils/captcha.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"time"
|
||||
)
|
||||
|
||||
var capdString = base64Captcha.NewDriverString(50, 150, 0, 5, 4, "123456789abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
|
||||
|
||||
var capdMath = base64Captcha.NewDriverMath(50, 150, 3, 10, nil, nil, nil)
|
||||
|
||||
type B64StringCaptchaProvider struct{}
|
||||
|
||||
func (p B64StringCaptchaProvider) Generate() (string, string, string, error) {
|
||||
id, content, answer := capdString.GenerateIdQuestionAnswer()
|
||||
return id, content, answer, nil
|
||||
}
|
||||
|
||||
func (p B64StringCaptchaProvider) Expiration() time.Duration {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
func (p B64StringCaptchaProvider) Draw(content string) (string, error) {
|
||||
item, err := capdString.DrawCaptcha(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b64str := item.EncodeB64string()
|
||||
return b64str, nil
|
||||
}
|
||||
|
||||
type B64MathCaptchaProvider struct{}
|
||||
|
||||
func (p B64MathCaptchaProvider) Generate() (string, string, string, error) {
|
||||
id, content, answer := capdMath.GenerateIdQuestionAnswer()
|
||||
return id, content, answer, nil
|
||||
}
|
||||
|
||||
func (p B64MathCaptchaProvider) Expiration() time.Duration {
|
||||
return 5 * time.Minute
|
||||
}
|
||||
func (p B64MathCaptchaProvider) Draw(content string) (string, error) {
|
||||
item, err := capdMath.DrawCaptcha(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b64str := item.EncodeB64string()
|
||||
return b64str, nil
|
||||
}
|
||||
296
utils/login_limiter.go
Normal file
296
utils/login_limiter.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 安全策略配置
|
||||
type SecurityPolicy struct {
|
||||
CaptchaThreshold int // 尝试失败次数达到验证码阈值,小于0表示不启用, 0表示强制启用
|
||||
BanThreshold int // 尝试失败次数达到封禁阈值,为0表示不启用
|
||||
AttemptsWindow time.Duration
|
||||
BanDuration time.Duration
|
||||
}
|
||||
|
||||
// 验证码提供者接口
|
||||
type CaptchaProvider interface {
|
||||
Generate() (id string, content string, answer string, err error)
|
||||
//Validate(ip, code string) bool
|
||||
Expiration() time.Duration // 验证码过期时间, 应该小于 AttemptsWindow
|
||||
Draw(content string) (string, error) // 绘制验证码
|
||||
}
|
||||
|
||||
// 验证码元数据
|
||||
type CaptchaMeta struct {
|
||||
Id string
|
||||
Content string
|
||||
Answer string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// IP封禁记录
|
||||
type BanRecord struct {
|
||||
ExpiresAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
// 登录限制器
|
||||
type LoginLimiter struct {
|
||||
mu sync.Mutex
|
||||
policy SecurityPolicy
|
||||
attempts map[string][]time.Time //
|
||||
captchas map[string]CaptchaMeta
|
||||
bannedIPs map[string]BanRecord
|
||||
provider CaptchaProvider
|
||||
cleanupStop chan struct{}
|
||||
}
|
||||
|
||||
var defaultSecurityPolicy = SecurityPolicy{
|
||||
CaptchaThreshold: 3,
|
||||
BanThreshold: 5,
|
||||
AttemptsWindow: 5 * time.Minute,
|
||||
BanDuration: 30 * time.Minute,
|
||||
}
|
||||
|
||||
func NewLoginLimiter(policy SecurityPolicy) *LoginLimiter {
|
||||
// 设置默认值
|
||||
if policy.AttemptsWindow == 0 {
|
||||
policy.AttemptsWindow = 5 * time.Minute
|
||||
}
|
||||
if policy.BanDuration == 0 {
|
||||
policy.BanDuration = 30 * time.Minute
|
||||
}
|
||||
|
||||
ll := &LoginLimiter{
|
||||
policy: policy,
|
||||
attempts: make(map[string][]time.Time),
|
||||
captchas: make(map[string]CaptchaMeta),
|
||||
bannedIPs: make(map[string]BanRecord),
|
||||
cleanupStop: make(chan struct{}),
|
||||
}
|
||||
go ll.cleanupRoutine()
|
||||
return ll
|
||||
}
|
||||
|
||||
// 注册验证码提供者
|
||||
func (ll *LoginLimiter) RegisterProvider(p CaptchaProvider) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
ll.provider = p
|
||||
}
|
||||
|
||||
// isDisabled 检查是否禁用登录限制
|
||||
func (ll *LoginLimiter) isDisabled() bool {
|
||||
return ll.policy.CaptchaThreshold < 0 && ll.policy.BanThreshold == 0
|
||||
}
|
||||
|
||||
// 记录登录失败尝试
|
||||
func (ll *LoginLimiter) RecordFailedAttempt(ip string) {
|
||||
if ll.isDisabled() {
|
||||
return
|
||||
}
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
if banned, _ := ll.isBanned(ip); banned {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
windowStart := now.Add(-ll.policy.AttemptsWindow)
|
||||
|
||||
// 清理过期尝试
|
||||
validAttempts := ll.pruneAttempts(ip, windowStart)
|
||||
|
||||
// 记录新尝试
|
||||
validAttempts = append(validAttempts, now)
|
||||
ll.attempts[ip] = validAttempts
|
||||
|
||||
// 检查封禁条件
|
||||
if ll.policy.BanThreshold > 0 && len(validAttempts) >= ll.policy.BanThreshold {
|
||||
ll.banIP(ip, "excessive failed attempts")
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
func (ll *LoginLimiter) RequireCaptcha() (error, CaptchaMeta) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
if ll.provider == nil {
|
||||
return errors.New("no captcha provider available"), CaptchaMeta{}
|
||||
}
|
||||
|
||||
id, content, answer, err := ll.provider.Generate()
|
||||
if err != nil {
|
||||
return err, CaptchaMeta{}
|
||||
}
|
||||
|
||||
// 存储验证码
|
||||
ll.captchas[id] = CaptchaMeta{
|
||||
Id: id,
|
||||
Content: content,
|
||||
Answer: answer,
|
||||
ExpiresAt: time.Now().Add(ll.provider.Expiration()),
|
||||
}
|
||||
|
||||
return nil, ll.captchas[id]
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
func (ll *LoginLimiter) VerifyCaptcha(id, answer string) bool {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
// 查找匹配验证码
|
||||
if ll.provider == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取并验证验证码
|
||||
captcha, exists := ll.captchas[id]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// 清理过期验证码
|
||||
if time.Now().After(captcha.ExpiresAt) {
|
||||
delete(ll.captchas, id)
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证并清理状态
|
||||
if answer == captcha.Answer {
|
||||
delete(ll.captchas, id)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ll *LoginLimiter) DrawCaptcha(content string) (err error, str string) {
|
||||
str, err = ll.provider.Draw(content)
|
||||
return
|
||||
}
|
||||
|
||||
// 清除记录窗口
|
||||
func (ll *LoginLimiter) RemoveAttempts(ip string) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
_, exists := ll.attempts[ip]
|
||||
if exists {
|
||||
delete(ll.attempts, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSecurityStatus 检查安全状态
|
||||
func (ll *LoginLimiter) CheckSecurityStatus(ip string) (banned bool, captchaRequired bool) {
|
||||
if ll.isDisabled() {
|
||||
return
|
||||
}
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
// 检查封禁状态
|
||||
if banned, _ = ll.isBanned(ip); banned {
|
||||
return
|
||||
}
|
||||
|
||||
// 清理过期数据
|
||||
ll.pruneAttempts(ip, time.Now().Add(-ll.policy.AttemptsWindow))
|
||||
|
||||
// 检查验证码要求
|
||||
captchaRequired = len(ll.attempts[ip]) >= ll.policy.CaptchaThreshold
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 后台清理任务
|
||||
func (ll *LoginLimiter) cleanupRoutine() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ll.cleanupExpired()
|
||||
case <-ll.cleanupStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内部工具方法
|
||||
func (ll *LoginLimiter) isBanned(ip string) (bool, BanRecord) {
|
||||
record, exists := ll.bannedIPs[ip]
|
||||
if !exists {
|
||||
return false, BanRecord{}
|
||||
}
|
||||
if time.Now().After(record.ExpiresAt) {
|
||||
delete(ll.bannedIPs, ip)
|
||||
return false, BanRecord{}
|
||||
}
|
||||
return true, record
|
||||
}
|
||||
|
||||
func (ll *LoginLimiter) banIP(ip, reason string) {
|
||||
ll.bannedIPs[ip] = BanRecord{
|
||||
ExpiresAt: time.Now().Add(ll.policy.BanDuration),
|
||||
Reason: reason,
|
||||
}
|
||||
delete(ll.attempts, ip)
|
||||
delete(ll.captchas, ip)
|
||||
}
|
||||
|
||||
func (ll *LoginLimiter) pruneAttempts(ip string, cutoff time.Time) []time.Time {
|
||||
var valid []time.Time
|
||||
for _, t := range ll.attempts[ip] {
|
||||
if t.After(cutoff) {
|
||||
valid = append(valid, t)
|
||||
}
|
||||
}
|
||||
if len(valid) == 0 {
|
||||
delete(ll.attempts, ip)
|
||||
} else {
|
||||
ll.attempts[ip] = valid
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
func (ll *LoginLimiter) pruneCaptchas(id string) {
|
||||
if captcha, exists := ll.captchas[id]; exists {
|
||||
if time.Now().After(captcha.ExpiresAt) {
|
||||
delete(ll.captchas, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ll *LoginLimiter) cleanupExpired() {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 清理封禁记录
|
||||
for ip, record := range ll.bannedIPs {
|
||||
if now.After(record.ExpiresAt) {
|
||||
delete(ll.bannedIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理尝试记录
|
||||
for ip := range ll.attempts {
|
||||
ll.pruneAttempts(ip, now.Add(-ll.policy.AttemptsWindow))
|
||||
}
|
||||
|
||||
// 清理验证码
|
||||
for id := range ll.captchas {
|
||||
ll.pruneCaptchas(id)
|
||||
}
|
||||
}
|
||||
290
utils/login_limiter_test.go
Normal file
290
utils/login_limiter_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockCaptchaProvider struct{}
|
||||
|
||||
func (p *MockCaptchaProvider) Generate() (string, string, string, error) {
|
||||
id := uuid.New().String()
|
||||
content := uuid.New().String()
|
||||
answer := uuid.New().String()
|
||||
return id, content, answer, nil
|
||||
}
|
||||
|
||||
func (p *MockCaptchaProvider) Expiration() time.Duration {
|
||||
return 2 * time.Second
|
||||
}
|
||||
func (p *MockCaptchaProvider) Draw(content string) (string, error) {
|
||||
return "MOCK", nil
|
||||
}
|
||||
|
||||
func TestSecurityWorkflow(t *testing.T) {
|
||||
policy := SecurityPolicy{
|
||||
CaptchaThreshold: 3,
|
||||
BanThreshold: 5,
|
||||
AttemptsWindow: 5 * time.Minute,
|
||||
BanDuration: 5 * time.Minute,
|
||||
}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "192.168.1.100"
|
||||
|
||||
// 测试正常失败记录
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
isBanned, capRequired := limiter.CheckSecurityStatus(ip)
|
||||
fmt.Printf("IP: %s, Banned: %v, Captcha Required: %v\n", ip, isBanned, capRequired)
|
||||
if isBanned {
|
||||
t.Error("IP should not be banned yet")
|
||||
}
|
||||
if !capRequired {
|
||||
t.Error("Captcha should be required")
|
||||
}
|
||||
// 测试触发封禁
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
isBanned, capRequired = limiter.CheckSecurityStatus(ip)
|
||||
fmt.Printf("IP: %s, Banned: %v, Captcha Required: %v\n", ip, isBanned, capRequired)
|
||||
}
|
||||
|
||||
// 测试封禁状态
|
||||
if isBanned, _ = limiter.CheckSecurityStatus(ip); !isBanned {
|
||||
t.Error("IP should be banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{CaptchaThreshold: 2}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
|
||||
// 验证成功
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
}
|
||||
|
||||
// 验证已删除
|
||||
if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该已删除")
|
||||
}
|
||||
|
||||
limiter.RemoveAttempts(ip)
|
||||
// 验证后状态
|
||||
if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
|
||||
t.Error("验证成功后应该重置状态")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaMustFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{CaptchaThreshold: 0}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
|
||||
// 验证成功
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
}
|
||||
|
||||
// 验证后状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
}
|
||||
func TestAttemptTimeout(t *testing.T) {
|
||||
policy := SecurityPolicy{CaptchaThreshold: 2, AttemptsWindow: 1 * time.Second}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
err, _ := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
}
|
||||
// 等待超过 AttemptsWindow
|
||||
time.Sleep(2 * time.Second)
|
||||
// 触发验证码要求
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); need {
|
||||
t.Error("不应该需要验证码")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptchaTimeout(t *testing.T) {
|
||||
policy := SecurityPolicy{CaptchaThreshold: 2}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
}
|
||||
|
||||
// 等待超过 CaptchaValidPeriod
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// 验证成功
|
||||
if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该已过期")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBanFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 5}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); !banned {
|
||||
t.Error("should be banned")
|
||||
}
|
||||
}
|
||||
func TestBanDisableFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 0}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); banned {
|
||||
t.Error("should not be banned")
|
||||
}
|
||||
}
|
||||
func TestBanTimeout(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 5, BanDuration: 1 * time.Second}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
// 触发ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 检查状态
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); banned {
|
||||
t.Error("should not be banned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiterDisabled(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 0, CaptchaThreshold: -1}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if banned, capNeed := limiter.CheckSecurityStatus(ip); banned || capNeed {
|
||||
fmt.Printf("IP: %s, Banned: %v, Captcha Required: %v\n", ip, banned, capNeed)
|
||||
t.Error("should not be banned or need captcha")
|
||||
}
|
||||
}
|
||||
|
||||
func TestB64CaptchaFlow(t *testing.T) {
|
||||
limiter := NewLoginLimiter(defaultSecurityPolicy)
|
||||
limiter.RegisterProvider(B64StringCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
|
||||
//draw
|
||||
err, b64 := limiter.DrawCaptcha(capc.Content)
|
||||
if err != nil {
|
||||
t.Fatalf("绘制验证码失败: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", b64)
|
||||
|
||||
// 验证成功
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
}
|
||||
limiter.RemoveAttempts(ip)
|
||||
// 验证后状态
|
||||
if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
|
||||
t.Error("验证成功后应该重置状态")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Md5(str string) string {
|
||||
@@ -100,3 +101,11 @@ func InArray(k string, arr []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func StringConcat(strs ...string) string {
|
||||
var builder strings.Builder
|
||||
for _, str := range strs {
|
||||
builder.WriteString(str)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user