diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 3cbc8ac87..d2c8786ee 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -18,6 +18,8 @@ type SettingInfo struct { ServerPort string `json:"serverPort"` SSL string `json:"ssl"` SSLType string `json:"sslType"` + BindDomain string `json:"bindDomain"` + AllowIPs string `json:"allowIPs"` SecurityEntrance string `json:"securityEntrance"` ExpirationDays string `json:"expirationDays"` ExpirationTime string `json:"expirationTime"` diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index e8c25bc27..3954f638d 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -67,6 +67,12 @@ func (u *SettingService) Update(key, value string) error { return err } } + if key == "BindDomain" { + global.CONF.System.BindDomain = value + } + if key == "AllowIPs" { + global.CONF.System.AllowIPs = value + } if err := settingRepo.Update(key, value); err != nil { return err } diff --git a/backend/configs/system.go b/backend/configs/system.go index 51a540593..4d6ca6c69 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -21,4 +21,6 @@ type System struct { IsDemo bool `mapstructure:"is_demo"` AppRepo string `mapstructure:"app_repo"` ChangeUserInfo bool `mapstructure:"change_user_info"` + AllowIPs string `mapstructure:"allow_ips"` + BindDomain string `mapstructure:"bind_domain"` } diff --git a/backend/constant/errs.go b/backend/constant/errs.go index fb3ce4723..625c6cb0b 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -14,6 +14,8 @@ const ( CodePasswordExpired = 405 CodeAuth = 406 CodeGlobalLoading = 407 + CodeErrIP = 408 + CodeErrDomain = 409 CodeErrInternalServer = 500 CodeErrHeader = 406 ) diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index b18bc06b3..012cc6eca 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -26,6 +26,18 @@ func Init() { } global.CONF.System.SSL = sslSetting.Value + ipsSetting, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs")) + if err != nil { + global.LOG.Errorf("load allow ips from setting failed, err: %v", err) + } + global.CONF.System.AllowIPs = ipsSetting.Value + + domainSetting, err := settingRepo.Get(settingRepo.WithByKey("BindDomain")) + if err != nil { + global.LOG.Errorf("load bind domain from setting failed, err: %v", err) + } + global.CONF.System.BindDomain = domainSetting.Value + if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { _ = settingRepo.Create("SystemStatus", "Free") } diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index f61dfd91a..d9733119b 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -28,6 +28,7 @@ func Init() { migrations.AddEntranceAndSSL, migrations.UpdateTableSetting, migrations.UpdateTableAppDetail, + migrations.AddBindAndAllowIPs, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index a8747124d..7bc85f642 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -335,3 +335,16 @@ var UpdateTableAppDetail = &gormigrate.Migration{ return nil }, } + +var AddBindAndAllowIPs = &gormigrate.Migration{ + ID: "20230414-add-bind-and-allow", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "BindDomain", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "AllowIPs", Value: ""}).Error; err != nil { + return err + } + return nil + }, +} diff --git a/backend/init/router/router.go b/backend/init/router/router.go index ceeea8b2b..fd41ac4b3 100644 --- a/backend/init/router/router.go +++ b/backend/init/router/router.go @@ -63,6 +63,8 @@ func Routers() *gin.Engine { }) } PrivateGroup := Router.Group("/api/v1") + PrivateGroup.Use(middleware.WhiteAllow()) + PrivateGroup.Use(middleware.BindDomain()) PrivateGroup.Use(middleware.GlobalLoading()) { systemRouter.InitBaseRouter(PrivateGroup) diff --git a/backend/middleware/bind_domain.go b/backend/middleware/bind_domain.go new file mode 100644 index 000000000..d614224a5 --- /dev/null +++ b/backend/middleware/bind_domain.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "errors" + "strings" + + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/gin-gonic/gin" +) + +func BindDomain() gin.HandlerFunc { + return func(c *gin.Context) { + if len(global.CONF.System.BindDomain) == 0 { + c.Next() + return + } + domains := c.Request.Host + parts := strings.Split(c.Request.Host, ":") + if len(parts) > 0 { + domains = parts[0] + } + + if domains != global.CONF.System.BindDomain { + helper.ErrorWithDetail(c, constant.CodeErrDomain, constant.ErrTypeInternalServer, errors.New("domain not allowed")) + return + } + c.Next() + } +} diff --git a/backend/middleware/ip_limit.go b/backend/middleware/ip_limit.go new file mode 100644 index 000000000..9e7e7745c --- /dev/null +++ b/backend/middleware/ip_limit.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "errors" + "strings" + + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/gin-gonic/gin" +) + +func WhiteAllow() gin.HandlerFunc { + return func(c *gin.Context) { + if len(global.CONF.System.AllowIPs) == 0 { + c.Next() + return + } + clientIP := c.ClientIP() + for _, ip := range strings.Split(global.CONF.System.AllowIPs, ",") { + if len(ip) != 0 && ip == clientIP { + c.Next() + return + } + } + helper.ErrorWithDetail(c, constant.CodeErrIP, constant.ErrTypeInternalServer, errors.New("IP address not allowed")) + } +} diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 3cb9f717c..23301b829 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -71,6 +71,8 @@ declare module 'vue' { ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElUpload: typeof import('element-plus/es')['ElUpload'] + Err_domain: typeof import('./src/components/error-message/err_domain.vue')['default'] + Err_ip: typeof import('./src/components/error-message/err_ip.vue')['default'] FileList: typeof import('./src/components/file-list/index.vue')['default'] FileRole: typeof import('./src/components/file-role/index.vue')['default'] Footer: typeof import('./src/components/app-layout/footer/index.vue')['default'] diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 9767e72fe..cccaa8efe 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -49,9 +49,21 @@ class RequestHttp { }); return Promise.reject(data); } - if (data.code == ResultEnum.EXPIRED) { - router.push({ name: 'Expired' }); - return data; + if (data.code == ResultEnum.ERRIP) { + globalStore.setLogStatus(false); + router.push({ + name: 'entrance', + params: { code: 'err-ip' }, + }); + return Promise.reject(data); + } + if (data.code == ResultEnum.ERRDOMAIN) { + globalStore.setLogStatus(false); + router.push({ + name: 'entrance', + params: { code: 'err-domain' }, + }); + return Promise.reject(data); } if (data.code == ResultEnum.ERRGLOBALLOADDING) { globalStore.setGlobalLoading(true); diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 1ff84d4bc..2d73692a9 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -17,6 +17,8 @@ export namespace Setting { serverPort: number; ssl: string; sslType: string; + allowIPs: string; + bindDomain: string; securityEntrance: string; expirationDays: number; expirationTime: string; diff --git a/frontend/src/components/error-message/err_domain.vue b/frontend/src/components/error-message/err_domain.vue new file mode 100644 index 000000000..1ff6d7c23 --- /dev/null +++ b/frontend/src/components/error-message/err_domain.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/error-message/err_ip.vue b/frontend/src/components/error-message/err_ip.vue new file mode 100644 index 000000000..be17e835d --- /dev/null +++ b/frontend/src/components/error-message/err_ip.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/src/enums/http-enum.ts b/frontend/src/enums/http-enum.ts index 5877ca7cb..b8086b693 100644 --- a/frontend/src/enums/http-enum.ts +++ b/frontend/src/enums/http-enum.ts @@ -7,6 +7,8 @@ export enum ResultEnum { EXPIRED = 405, ERRAUTH = 406, ERRGLOBALLOADDING = 407, + ERRIP = 408, + ERRDOMAIN = 409, TIMEOUT = 20000, TYPE = 'success', } diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a7e877d3d..0540b70b7 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -115,6 +115,9 @@ const message = { notSafe: 'Access Denied', safeEntrance1: 'The secure login has been enabled in the current environment', safeEntrance2: 'Enter the following command on the SSH terminal to view the panel entry: 1pctl user-info', + errIP1: 'Authorized IP address access is enabled in the current environment', + errDomain1: 'Access domain name binding is enabled in the current environment', + errHelper: 'To reset the binding information, run the following command on the SSH terminal: ', codeInput: 'Please enter the 6-digit verification code of the MFA validator', mfaTitle: 'MFA Certification', mfaCode: 'MFA verification code', @@ -933,6 +936,19 @@ const message = { complexity: 'Complexity verification', complexityHelper: 'The password must contain at least eight characters and contain at least three uppercase letters, lowercase letters, digits, and special characters', + + bindDomain: 'Bind domain', + bindDomainHelper: + 'After the domain binding, only the domain in the setting can be used to access 1Panel service', + bindDomainHelper1: 'If the binding domain is empty, the binding of the domain is cancelled', + bindDomainWarnning: + 'If the binding domain is empty, the binding of the domain is cancelled. Do you want to continue?', + allowIPs: 'Authorized IP', + allowIPsHelper: + 'After setting the authorized IP address, only the IP address in the setting can access the 1Panel service', + allowIPsWarnning: + '设After setting the authorized IP address, only the IP address in the setting can access the 1Panel service. Do you want to continue?', + allowIPsHelper1: 'If the authorized IP address is empty, the authorized IP address is canceled', mfa: 'MFA', mfaAlert: 'MFA password is generated based on the current time. Please ensure that the server time is synchronized.', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 02b3d600c..f62a42da0 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -119,6 +119,9 @@ const message = { notSafe: '暂无权限访问', safeEntrance1: '当前环境已经开启了安全入口登录', safeEntrance2: '在 SSH 终端输入以下命令来查看面板入口: 1pctl user-info', + errIP1: '当前环境已经开启了授权 IP 访问', + errDomain1: '当前环境已经开启了访问域名绑定', + errHelper: '可在 SSH 终端输入以下命令来重置绑定信息: ', codeInput: '请输入 MFA 验证器的 6 位验证码', mfaTitle: 'MFA 认证', mfaCode: 'MFA 验证码', @@ -965,6 +968,14 @@ const message = { timeoutHelper: '【 {0} 天后 】面板密码即将过期,过期后需要重新设置密码', complexity: '密码复杂度验证', complexityHelper: '开启后密码必须满足密码长度大于 8 位且包含字母、数字及特殊字符', + bindDomain: '域名绑定', + bindDomainHelper: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务', + bindDomainHelper1: '绑定域名为空时,则取消域名绑定', + bindDomainWarnning: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务,是否继续?', + allowIPs: '授权 IP', + allowIPsHelper: '设置授权 IP 后,仅有设置中的 IP 可以访问 1Panel 服务', + allowIPsWarnning: '设置授权 IP 后,仅有设置中的 IP 可以访问 1Panel 服务,是否继续?', + allowIPsHelper1: '授权 IP 为空时,则取消授权 IP', mfa: '两步验证', mfaAlert: '两步验证密码是基于当前时间生成,请确保服务器时间已同步', mfaHelper: '开启后会验证手机应用验证码', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 53d0ef1eb..bccd6d18a 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -158,6 +158,9 @@ export function getIcon(extention: string): string { } export function checkIp(value: string): boolean { + if (value === '') { + return true; + } const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; if (!reg.test(value) && value !== '') { diff --git a/frontend/src/views/login/entrance/index.vue b/frontend/src/views/login/entrance/index.vue index a096cdd44..aafb87c3b 100644 --- a/frontend/src/views/login/entrance/index.vue +++ b/frontend/src/views/login/entrance/index.vue @@ -1,6 +1,6 @@ @@ -25,12 +31,15 @@ import { checkIsSafety } from '@/api/modules/auth'; import LoginForm from '../components/login-form.vue'; import UnSafe from '@/components/error-message/unsafe.vue'; +import ErrIP from '@/components/error-message/err_ip.vue'; +import ErrDomain from '@/components/error-message/err_domain.vue'; import { ref, onMounted } from 'vue'; import { GlobalStore } from '@/store'; const globalStore = GlobalStore(); const isSafety = ref(true); const screenWidth = ref(null); +const isErr = ref(); const mySafetyCode = defineProps({ code: { @@ -41,7 +50,13 @@ const mySafetyCode = defineProps({ }); const getStatus = async () => { + if (mySafetyCode.code === 'err-ip' || mySafetyCode.code === 'err-domain') { + isErr.value = true; + } const res = await checkIsSafety(mySafetyCode.code); + if (mySafetyCode.code === 'err-ip' || mySafetyCode.code === 'err-domain') { + isErr.value = false; + } isSafety.value = res.data; if (isSafety.value) { globalStore.entrance = mySafetyCode.code; diff --git a/frontend/src/views/setting/safe/allowips/index.vue b/frontend/src/views/setting/safe/allowips/index.vue new file mode 100644 index 000000000..533bb7582 --- /dev/null +++ b/frontend/src/views/setting/safe/allowips/index.vue @@ -0,0 +1,133 @@ + + diff --git a/frontend/src/views/setting/safe/domain/index.vue b/frontend/src/views/setting/safe/domain/index.vue new file mode 100644 index 000000000..07767a6d0 --- /dev/null +++ b/frontend/src/views/setting/safe/domain/index.vue @@ -0,0 +1,106 @@ + + diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index e6eedab58..a6c6b4bcc 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -39,6 +39,42 @@ {{ $t('setting.entranceHelper') }} + + + + + + + + {{ $t('setting.allowIPsHelper') }} + + + + + + + + + + {{ $t('setting.bindDomainHelper') }} + + @@ -123,6 +161,8 @@ import SSLSetting from '@/views/setting/safe/ssl/index.vue'; import MfaSetting from '@/views/setting/safe/mfa/index.vue'; import TimeoutSetting from '@/views/setting/safe/timeout/index.vue'; import EntranceSetting from '@/views/setting/safe/entrance/index.vue'; +import DomainSetting from '@/views/setting/safe/domain/index.vue'; +import AllowIPsSetting from '@/views/setting/safe/allowips/index.vue'; import { updateSetting, getSettingInfo, getSystemAvailable, updateSSL, loadSSLInfo } from '@/api/modules/setting'; import i18n from '@/lang'; import { MsgSuccess } from '@/utils/message'; @@ -136,6 +176,8 @@ const mfaRef = ref(); const sslRef = ref(); const sslInfo = ref(); +const domainRef = ref(); +const allowIPsRef = ref(); const form = reactive({ serverPort: 9999, @@ -146,6 +188,8 @@ const form = reactive({ expirationTime: '', complexityVerification: 'disable', mfaStatus: 'disable', + allowIPs: '', + bindDomain: '', }); const unset = ref(i18n.global.t('setting.unSetting')); @@ -163,6 +207,8 @@ const search = async () => { form.expirationTime = res.data.expirationTime; form.complexityVerification = res.data.complexityVerification; form.mfaStatus = res.data.mfaStatus; + form.allowIPs = res.data.allowIPs || ''; + form.bindDomain = res.data.bindDomain; }; const onSaveComplexity = async () => { @@ -199,12 +245,18 @@ const handleMFA = async () => { }); }; -const onChangeEntrance = async () => { +const onChangeEntrance = () => { entranceRef.value.acceptParams({ securityEntrance: form.securityEntrance }); }; -const onChangePort = async () => { +const onChangePort = () => { portRef.value.acceptParams({ serverPort: form.serverPort }); }; +const onChangeBindDomain = () => { + domainRef.value.acceptParams({ bindDomain: form.bindDomain }); +}; +const onChangeAllowIPs = () => { + allowIPsRef.value.acceptParams({ allowIPs: form.allowIPs }); +}; const handleSSL = async () => { if (form.ssl === 'enable') { let params = {