diff --git a/internal/securitymanager/security_config.go b/internal/securitymanager/security_config.go index 15b15eaa..dca9f0bd 100644 --- a/internal/securitymanager/security_config.go +++ b/internal/securitymanager/security_config.go @@ -22,7 +22,10 @@ const ( var sharedSecurityConfig *SecurityConfig = nil type SecurityConfig struct { - Frame string `json:"frame"` + Frame string `json:"frame"` + AllowCountryIds []int64 `json:"allowCountryIds"` + AllowProvinceIds []int64 `json:"allowProvinceIds"` + AllowLocal bool `json:"allowLocal"` } func LoadSecurityConfig() (*SecurityConfig, error) { @@ -50,7 +53,7 @@ func UpdateSecurityConfig(securityConfig *SecurityConfig) error { if err != nil { return err } - _, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(1), &pb.UpdateSysSettingRequest{ + _, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{ Code: SecuritySettingName, ValueJSON: valueJSON, }) @@ -69,7 +72,7 @@ func loadSecurityConfig() (*SecurityConfig, error) { if err != nil { return nil, err } - resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(1), &pb.ReadSysSettingRequest{ + resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{ Code: SecuritySettingName, }) if err != nil { @@ -93,6 +96,7 @@ func loadSecurityConfig() (*SecurityConfig, error) { func defaultSecurityConfig() *SecurityConfig { return &SecurityConfig{ - Frame: FrameSameOrigin, + Frame: FrameSameOrigin, + AllowLocal: true, } } diff --git a/internal/tasks/task_sync_cluster.go b/internal/tasks/task_sync_cluster.go index 69fdb680..df4f22b3 100644 --- a/internal/tasks/task_sync_cluster.go +++ b/internal/tasks/task_sync_cluster.go @@ -41,7 +41,7 @@ func (this *SyncClusterTask) loop() error { if err != nil { return err } - ctx := rpcClient.Context(1) + ctx := rpcClient.Context(0) resp, err := rpcClient.NodeClusterRPC().FindAllChangedNodeClusters(ctx, &pb.FindAllChangedNodeClustersRequest{}) if err != nil { return err diff --git a/internal/utils/strings.go b/internal/utils/strings.go index d17693b6..1c83b53c 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -1,6 +1,9 @@ package utils -import "strings" +import ( + "github.com/iwind/TeaGo/types" + "strings" +) // format address func FormatAddress(addr string) string { @@ -13,3 +16,16 @@ func FormatAddress(addr string) string { addr = strings.TrimSpace(addr) return addr } + +// 分割数字 +func SplitNumbers(numbers string) (result []int64) { + if len(numbers) == 0 { + return + } + pieces := strings.Split(numbers, ",") + for _, piece := range pieces { + number := types.Int64(strings.TrimSpace(piece)) + result = append(result, number) + } + return +} diff --git a/internal/web/actions/default/settings/security/index.go b/internal/web/actions/default/settings/security/index.go index 99a26b7f..68f4d100 100644 --- a/internal/web/actions/default/settings/security/index.go +++ b/internal/web/actions/default/settings/security/index.go @@ -1,9 +1,12 @@ package security import ( + "encoding/json" "github.com/TeaOSLab/EdgeAdmin/internal/securitymanager" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/maps" ) type IndexAction struct { @@ -20,12 +23,52 @@ func (this *IndexAction) RunGet(params struct{}) { this.ErrorPage(err) return } + + // 国家和地区 + countryMaps := []maps.Map{} + for _, countryId := range config.AllowCountryIds { + countryResp, err := this.RPC().RegionCountryRPC().FindEnabledRegionCountry(this.AdminContext(), &pb.FindEnabledRegionCountryRequest{CountryId: countryId}) + if err != nil { + this.ErrorPage(err) + return + } + country := countryResp.Country + if country != nil { + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + }) + } + } + this.Data["countries"] = countryMaps + + // 省份 + provinceMaps := []maps.Map{} + for _, provinceId := range config.AllowProvinceIds { + provinceResp, err := this.RPC().RegionProvinceRPC().FindEnabledRegionProvince(this.AdminContext(), &pb.FindEnabledRegionProvinceRequest{ProvinceId: provinceId}) + if err != nil { + this.ErrorPage(err) + return + } + province := provinceResp.Province + if province != nil { + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + }) + } + } + this.Data["provinces"] = provinceMaps + this.Data["config"] = config this.Show() } func (this *IndexAction) RunPost(params struct { - Frame string + Frame string + CountryIdsJSON []byte + ProvinceIdsJSON []byte + AllowLocal bool Must *actions.Must CSRF *actionutils.CSRF @@ -38,7 +81,34 @@ func (this *IndexAction) RunPost(params struct { return } + // 框架 config.Frame = params.Frame + + // 国家和地区 + countryIds := []int64{} + if len(params.CountryIdsJSON) > 0 { + err = json.Unmarshal(params.CountryIdsJSON, &countryIds) + if err != nil { + this.ErrorPage(err) + return + } + } + config.AllowCountryIds = countryIds + + // 省份 + provinceIds := []int64{} + if len(params.ProvinceIdsJSON) > 0 { + err = json.Unmarshal(params.ProvinceIdsJSON, &provinceIds) + if err != nil { + this.ErrorPage(err) + return + } + } + config.AllowProvinceIds = provinceIds + + // 允许本地 + config.AllowLocal = params.AllowLocal + err = securitymanager.UpdateSecurityConfig(config) if err != nil { this.ErrorPage(err) diff --git a/internal/web/actions/default/ui/init.go b/internal/web/actions/default/ui/init.go index ea127400..2a519fad 100644 --- a/internal/web/actions/default/ui/init.go +++ b/internal/web/actions/default/ui/init.go @@ -11,6 +11,8 @@ func init() { server. Prefix("/ui"). Get("/download", new(DownloadAction)). + GetPost("/selectProvincesPopup", new(SelectProvincesPopupAction)). + GetPost("/selectCountriesPopup", new(SelectCountriesPopupAction)). // 以下的需要压缩 Helper(&actions.Gzip{Level: gzip.BestCompression}). diff --git a/internal/web/actions/default/ui/selectCountriesPopup.go b/internal/web/actions/default/ui/selectCountriesPopup.go new file mode 100644 index 00000000..23a833fb --- /dev/null +++ b/internal/web/actions/default/ui/selectCountriesPopup.go @@ -0,0 +1,70 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" + "strings" +) + +type SelectCountriesPopupAction struct { + actionutils.ParentAction +} + +func (this *SelectCountriesPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *SelectCountriesPopupAction) RunGet(params struct { + CountryIds string +}) { + selectedCountryIds := utils.SplitNumbers(params.CountryIds) + + countriesResp, err := this.RPC().RegionCountryRPC().FindAllEnabledRegionCountries(this.AdminContext(), &pb.FindAllEnabledRegionCountriesRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + countryMaps := []maps.Map{} + for _, country := range countriesResp.Countries { + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + "letter": strings.ToUpper(string(country.Pinyin[0][0])), + "isChecked": lists.ContainsInt64(selectedCountryIds, country.Id), + }) + } + this.Data["countries"] = countryMaps + + this.Show() +} + +func (this *SelectCountriesPopupAction) RunPost(params struct { + CountryIds []int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + countryMaps := []maps.Map{} + for _, countryId := range params.CountryIds { + countryResp, err := this.RPC().RegionCountryRPC().FindEnabledRegionCountry(this.AdminContext(), &pb.FindEnabledRegionCountryRequest{CountryId: countryId}) + if err != nil { + this.ErrorPage(err) + return + } + country := countryResp.Country + if country == nil { + continue + } + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + }) + } + this.Data["countries"] = countryMaps + + this.Success() +} diff --git a/internal/web/actions/default/ui/selectProvincesPopup.go b/internal/web/actions/default/ui/selectProvincesPopup.go new file mode 100644 index 00000000..e9448a90 --- /dev/null +++ b/internal/web/actions/default/ui/selectProvincesPopup.go @@ -0,0 +1,70 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" +) + +const ChinaCountryId = 1 + +type SelectProvincesPopupAction struct { + actionutils.ParentAction +} + +func (this *SelectProvincesPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *SelectProvincesPopupAction) RunGet(params struct { + ProvinceIds string +}) { + selectedProvinceIds := utils.SplitNumbers(params.ProvinceIds) + + provincesResp, err := this.RPC().RegionProvinceRPC().FindAllEnabledRegionProvincesWithCountryId(this.AdminContext(), &pb.FindAllEnabledRegionProvincesWithCountryIdRequest{CountryId: ChinaCountryId}) + if err != nil { + this.ErrorPage(err) + return + } + provinceMaps := []maps.Map{} + for _, province := range provincesResp.Provinces { + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + "isChecked": lists.ContainsInt64(selectedProvinceIds, province.Id), + }) + } + this.Data["provinces"] = provinceMaps + + this.Show() +} + +func (this *SelectProvincesPopupAction) RunPost(params struct { + ProvinceIds []int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + provinceMaps := []maps.Map{} + for _, provinceId := range params.ProvinceIds { + provinceResp, err := this.RPC().RegionProvinceRPC().FindEnabledRegionProvince(this.AdminContext(), &pb.FindEnabledRegionProvinceRequest{ProvinceId: provinceId}) + if err != nil { + this.ErrorPage(err) + return + } + province := provinceResp.Province + if province == nil { + continue + } + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + }) + } + this.Data["provinces"] = provinceMaps + + this.Success() +} diff --git a/internal/web/helpers/user_must_auth.go b/internal/web/helpers/user_must_auth.go index bdea340a..05c6f7f9 100644 --- a/internal/web/helpers/user_must_auth.go +++ b/internal/web/helpers/user_must_auth.go @@ -35,6 +35,12 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam } action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'") + // 检查IP + if !checkIP(securityConfig, action.RequestRemoteIP()) { + action.ResponseWriter.WriteHeader(http.StatusForbidden) + return false + } + // 检查系统是否已经配置过 if !setup.IsConfigured() { action.RedirectURL("/setup") @@ -82,8 +88,48 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam return action.Data["teaTitle"].(string) }) - // 初始化变量 - modules := []maps.Map{ + action.Data["teaTitle"] = teaconst.ProductNameZH + action.Data["teaName"] = teaconst.ProductNameZH + + resp, err := rpc.AdminRPC().FindAdminFullname(rpc.Context(0), &pb.FindAdminFullnameRequest{AdminId: int64(this.AdminId)}) + if err != nil { + utils.PrintError(err) + action.Data["teaUsername"] = "" + } else { + action.Data["teaUsername"] = resp.Fullname + } + + action.Data["teaUserAvatar"] = "" + + action.Data["teaMenu"] = "" + action.Data["teaModules"] = this.modules() + action.Data["teaSubMenus"] = []map[string]interface{}{} + action.Data["teaTabbar"] = []map[string]interface{}{} + action.Data["teaVersion"] = teaconst.Version + action.Data["teaIsSuper"] = false + action.Data["teaDemoEnabled"] = teaconst.IsDemo + if !action.Data.Has("teaSubMenu") { + action.Data["teaSubMenu"] = "" + } + + // 菜单 + action.Data["firstMenuItem"] = "" + + // 未读消息数 + action.Data["teaBadge"] = 0 + + // 调用Init + initMethod := reflect.ValueOf(actionPtr).MethodByName("Init") + if initMethod.IsValid() { + initMethod.Call([]reflect.Value{}) + } + + return true +} + +// 菜单配置 +func (this *UserMustAuth) modules() []maps.Map { + return []maps.Map{ { "code": "servers", "name": "网站服务", @@ -136,46 +182,9 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam "icon": "history", }, } - - action.Data["teaTitle"] = teaconst.ProductNameZH - action.Data["teaName"] = teaconst.ProductNameZH - - resp, err := rpc.AdminRPC().FindAdminFullname(rpc.Context(0), &pb.FindAdminFullnameRequest{AdminId: int64(this.AdminId)}) - if err != nil { - utils.PrintError(err) - action.Data["teaUsername"] = "" - } else { - action.Data["teaUsername"] = resp.Fullname - } - - action.Data["teaUserAvatar"] = "" - - action.Data["teaMenu"] = "" - action.Data["teaModules"] = modules - action.Data["teaSubMenus"] = []map[string]interface{}{} - action.Data["teaTabbar"] = []map[string]interface{}{} - action.Data["teaVersion"] = teaconst.Version - action.Data["teaIsSuper"] = false - action.Data["teaDemoEnabled"] = teaconst.IsDemo - if !action.Data.Has("teaSubMenu") { - action.Data["teaSubMenu"] = "" - } - - // 菜单 - action.Data["firstMenuItem"] = "" - - // 未读消息数 - action.Data["teaBadge"] = 0 - - // 调用Init - initMethod := reflect.ValueOf(actionPtr).MethodByName("Init") - if initMethod.IsValid() { - initMethod.Call([]reflect.Value{}) - } - - return true } +// 跳转到登录页 func (this *UserMustAuth) login(action *actions.ActionObject) { action.RedirectURL("/") } diff --git a/internal/web/helpers/user_should_auth.go b/internal/web/helpers/user_should_auth.go index 54bc9f0a..f78eab13 100644 --- a/internal/web/helpers/user_should_auth.go +++ b/internal/web/helpers/user_should_auth.go @@ -24,6 +24,12 @@ func (this *UserShouldAuth) BeforeAction(actionPtr actions.ActionWrapper, paramN } action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'") + // 检查IP + if !checkIP(securityConfig, action.RequestRemoteIP()) { + action.ResponseWriter.WriteHeader(http.StatusForbidden) + return false + } + return true } diff --git a/internal/web/helpers/utils.go b/internal/web/helpers/utils.go new file mode 100644 index 00000000..6eb10c37 --- /dev/null +++ b/internal/web/helpers/utils.go @@ -0,0 +1,60 @@ +package helpers + +import ( + nodes "github.com/TeaOSLab/EdgeAdmin/internal/rpc" + "github.com/TeaOSLab/EdgeAdmin/internal/securitymanager" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/logs" + "net" +) + +// 检查用户IP +func checkIP(config *securitymanager.SecurityConfig, ipAddr string) bool { + if config == nil { + return true + } + + // 本地IP + ip := net.ParseIP(ipAddr).To4() + if ip == nil { + logs.Println("[USER_MUST_AUTH]invalid client address: " + ipAddr) + return false + } + if config.AllowLocal && isLocalIP(ip) { + return true + } + + // 检查位置 + if len(config.AllowCountryIds) > 0 || len(config.AllowProvinceIds) > 0 { + rpc, err := nodes.SharedRPC() + if err != nil { + logs.Println("[USER_MUST_AUTH][ERROR]" + err.Error()) + return false + } + resp, err := rpc.IPLibraryRPC().LookupIPRegion(rpc.Context(0), &pb.LookupIPRegionRequest{Ip: ipAddr}) + if err != nil { + logs.Println("[USER_MUST_AUTH][ERROR]" + err.Error()) + return false + } + if resp.Region == nil { + return true + } + if len(config.AllowCountryIds) > 0 && !lists.ContainsInt64(config.AllowCountryIds, resp.Region.CountryId) { + return false + } + if len(config.AllowProvinceIds) > 0 && !lists.ContainsInt64(config.AllowProvinceIds, resp.Region.ProvinceId) { + return false + } + } + + return true +} + +// 判断是否为本地IP +func isLocalIP(ip net.IP) bool { + return ip[0] == 127 || + ip[0] == 10 || + (ip[0] == 172 && ip[1]&0xf0 == 16) || + (ip[0] == 192 && ip[1] == 168) +} diff --git a/web/public/js/components/common/countries-selector.js b/web/public/js/components/common/countries-selector.js new file mode 100644 index 00000000..cf423b06 --- /dev/null +++ b/web/public/js/components/common/countries-selector.js @@ -0,0 +1,51 @@ +Vue.component("countries-selector", { + props: ["v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + let countryIds = countries.$map(function (k, v) { + return v.id + }) + return { + countries: countries, + countryIds: countryIds + } + }, + methods: { + add: function () { + let countryStringIds = this.countryIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.countries = resp.data.countries + that.change() + } + }) + }, + remove: function (index) { + this.countries.$remove(index) + this.change() + }, + change: function () { + this.countryIds = this.countries.$map(function (k, v) { + return v.id + }) + } + }, + template: `
` +}) \ No newline at end of file diff --git a/web/public/js/components/common/provinces-selector.js b/web/public/js/components/common/provinces-selector.js new file mode 100644 index 00000000..e2bb458a --- /dev/null +++ b/web/public/js/components/common/provinces-selector.js @@ -0,0 +1,51 @@ +Vue.component("provinces-selector", { + props: ["v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + let provinceIds = provinces.$map(function (k, v) { + return v.id + }) + return { + provinces: provinces, + provinceIds: provinceIds + } + }, + methods: { + add: function () { + let provinceStringIds = this.provinceIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.provinces = resp.data.provinces + that.change() + } + }) + }, + remove: function (index) { + this.provinces.$remove(index) + this.change() + }, + change: function () { + this.provinceIds = this.provinces.$map(function (k, v) { + return v.id + }) + } + }, + template: `` +}) \ No newline at end of file diff --git a/web/views/@default/@layout.html b/web/views/@default/@layout.html index 0d0089d0..073bf342 100644 --- a/web/views/@default/@layout.html +++ b/web/views/@default/@layout.html @@ -12,7 +12,7 @@ {$TEA.VUE} {$echo "header"} - + diff --git a/web/views/@default/@layout_popup.html b/web/views/@default/@layout_popup.html index 3e78d08c..ccfdc186 100644 --- a/web/views/@default/@layout_popup.html +++ b/web/views/@default/@layout_popup.html @@ -12,7 +12,7 @@ {$echo "header"} - + diff --git a/web/views/@default/settings/security/index.html b/web/views/@default/settings/security/index.html index c2081fcf..1faac34b 100644 --- a/web/views/@default/settings/security/index.html +++ b/web/views/@default/settings/security/index.html @@ -15,6 +15,27 @@当前服务被别的网页框架嵌套的条件限制。
+设置后,只有这些国家和地区才能访问管理界面,如果不设置表示没有限制。
+设置后,只有这些省份才能访问管理界面,如果不设置表示没有限制。
+选中表示允许在本机和局域网访问。
+