Browse Source

feat: 增加流量限制功能

pull/35/head
zhengkunwang223 2 years ago committed by zhengkunwang223
parent
commit
d5aee147f4
  1. 34
      backend/app/dto/nginx.go
  2. 16
      backend/app/service/website.go
  3. 124
      backend/app/service/website_utils.go
  4. 50
      backend/utils/cmd/cmd.go
  5. 23
      backend/utils/nginx/components/block.go
  6. 36
      backend/utils/nginx/components/server.go
  7. 5
      cmd/server/nginx_conf/limit.conf
  8. 3
      cmd/server/nginx_conf/nginx_conf.go
  9. 8
      frontend/src/api/interface/website.ts
  10. 2
      frontend/src/api/modules/website.ts
  11. 7
      frontend/src/lang/modules/zh.ts
  12. 33
      frontend/src/views/website/project/config/basic/default-doc/index.vue
  13. 6
      frontend/src/views/website/project/config/basic/index.vue
  14. 135
      frontend/src/views/website/project/config/basic/limit-conn/index.vue

34
backend/app/dto/nginx.go

@ -10,17 +10,41 @@ type NginxConfig struct {
}
type NginxConfigReq struct {
Scope NginxScope `json:"scope"`
WebSiteID uint `json:"webSiteId" validate:"required"`
Params map[string]string `json:"params"`
Scope NginxScope `json:"scope"`
Operate NginxOp `json:"operate"`
WebSiteID uint `json:"webSiteId" validate:"required"`
Params interface{} `json:"params"`
}
type NginxScope string
const (
Index NginxScope = "index"
Index NginxScope = "index"
LimitConn NginxScope = "limit-conn"
)
type NginxOp string
const (
ConfigNew NginxOp = "add"
ConfigUpdate NginxOp = "update"
ConfigDel NginxOp = "delete"
)
var ScopeKeyMap = map[NginxScope][]string{
Index: {"index"},
Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
}
var RepeatKeys = map[string]struct {
}{
"limit_conn": {},
"limit_conn_zone": {},
}
type NginxParam struct {
Name string `json:"name"`
SecondKey string `json:"secondKey"`
IsRepeatKey bool `json:"isRepeatKey"`
Params []string `json:"params"`
}

16
backend/app/service/website.go

@ -186,7 +186,7 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
return websiteDomainRepo.DeleteBy(context.TODO(), commonRepo.WithByID(domainId))
}
func (w WebsiteService) GetNginxConfigByScope(req dto.NginxConfigReq) (map[string]interface{}, error) {
func (w WebsiteService) GetNginxConfigByScope(req dto.NginxConfigReq) ([]dto.NginxParam, error) {
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
@ -207,19 +207,13 @@ func (w WebsiteService) UpdateNginxConfigByScope(req dto.NginxConfigReq) error {
if !ok || len(keys) == 0 {
return nil
}
keyValues := make(map[string][]string, len(keys))
for k, v := range req.Params {
for _, name := range keys {
if name == k {
keyValues[k] = getNginxParams(k, v)
}
}
}
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebSiteID))
if err != nil {
return err
}
if req.Operate == dto.ConfigDel {
return deleteNginxConfig(website, keys, req.Scope)
}
return updateNginxConfig(website, keyValues)
return updateNginxConfig(website, getNginxParams(req.Params, keys), req.Scope)
}

124
backend/app/service/website_utils.go

@ -91,8 +91,8 @@ func opNginx(containerName, operate string) error {
if operate == "check" {
nginxCmd = fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -t")
}
if _, err := cmd.Exec(nginxCmd); err != nil {
return err
if out, err := cmd.Exec(nginxCmd); err != nil {
return errors.New(out)
}
return nil
}
@ -206,36 +206,50 @@ func deleteListenAndServerName(website model.WebSite, ports []int, domains []str
return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxConfig.ContainerName)
}
func getNginxConfigByKeys(website model.WebSite, keys []string) (map[string]interface{}, error) {
func getNginxConfigByKeys(website model.WebSite, keys []string) ([]dto.NginxParam, error) {
nginxConfig, err := getNginxConfig(website.PrimaryDomain)
if err != nil {
return nil, err
}
config := nginxConfig.Config
server := config.FindServers()[0]
res := make(map[string]interface{})
var res []dto.NginxParam
for _, key := range keys {
dirs := server.FindDirectives(key)
for _, dir := range dirs {
res[dir.GetName()] = dir.GetParameters()
nginxParam := dto.NginxParam{
Name: dir.GetName(),
Params: dir.GetParameters(),
}
if isRepeatKey(key) {
nginxParam.IsRepeatKey = true
nginxParam.SecondKey = dir.GetParameters()[0]
}
res = append(res, nginxParam)
}
}
return res, nil
}
func updateNginxConfig(website model.WebSite, keyValues map[string][]string) error {
func updateNginxConfig(website model.WebSite, params []dto.NginxParam, scope dto.NginxScope) error {
nginxConfig, err := getNginxConfig(website.PrimaryDomain)
if err != nil {
return err
}
config := nginxConfig.Config
updateConfig(config, scope)
server := config.FindServers()[0]
for k, v := range keyValues {
for _, p := range params {
newDir := components.Directive{
Name: k,
Parameters: v,
Name: p.Name,
Parameters: p.Params,
}
if p.IsRepeatKey {
server.UpdateDirectiveBySecondKey(p.Name, p.SecondKey, newDir)
} else {
server.UpdateDirectives(p.Name, newDir)
}
server.UpdateDirectives(k, newDir)
}
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
@ -243,13 +257,101 @@ func updateNginxConfig(website model.WebSite, keyValues map[string][]string) err
return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxConfig.ContainerName)
}
func getNginxParams(key string, param interface{}) []string {
func updateConfig(config *components.Config, scope dto.NginxScope) {
if scope == dto.LimitConn {
limit := parser.NewStringParser(string(nginx_conf.Limit)).Parse()
for _, dir := range limit.GetDirectives() {
newDir := components.Directive{
Name: dir.GetName(),
Parameters: dir.GetParameters(),
}
config.UpdateDirectiveBySecondKey(dir.GetName(), dir.GetParameters()[0], newDir)
}
}
}
func deleteNginxConfig(website model.WebSite, keys []string, scope dto.NginxScope) error {
nginxConfig, err := getNginxConfig(website.PrimaryDomain)
if err != nil {
return err
}
config := nginxConfig.Config
config.RemoveDirectives(keys)
server := config.FindServers()[0]
server.RemoveDirectives(keys)
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
}
return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxConfig.ContainerName)
}
func getParamArray(key string, param interface{}) []string {
var res []string
switch param.(type) {
case string:
if key == "index" {
res = strings.Split(param.(string), "\n")
return res
}
res = strings.Split(param.(string), " ")
return res
}
return res
}
func handleParamMap(paramMap map[string]string, keys []string) []dto.NginxParam {
var nginxParams []dto.NginxParam
for k, v := range paramMap {
for _, name := range keys {
if name == k {
param := dto.NginxParam{
Name: k,
Params: getParamArray(k, v),
}
if isRepeatKey(k) {
param.IsRepeatKey = true
param.SecondKey = param.Params[0]
}
nginxParams = append(nginxParams, param)
}
}
}
return nginxParams
}
func getNginxParams(params interface{}, keys []string) []dto.NginxParam {
var nginxParams []dto.NginxParam
switch params.(type) {
case map[string]string:
return handleParamMap(params.(map[string]string), keys)
case []interface{}:
if mArray, ok := params.([]interface{}); ok {
for _, mA := range mArray {
if m, ok := mA.(map[string]interface{}); ok {
nginxParams = append(nginxParams, handleParamMap(toMapStr(m), keys)...)
}
}
}
}
return nginxParams
}
func isRepeatKey(key string) bool {
if _, ok := dto.RepeatKeys[key]; ok {
return true
}
return false
}
func toMapStr(m map[string]interface{}) map[string]string {
ret := make(map[string]string, len(m))
for k, v := range m {
ret[k] = fmt.Sprint(v)
}
return ret
}

50
backend/utils/cmd/cmd.go

@ -1,50 +1,18 @@
package cmd
import (
"bufio"
"context"
"io"
"bytes"
"os/exec"
"sync"
)
func Exec(cmdStr string) (out string, err error) {
command := exec.CommandContext(context.Background(), "bash", "-c", cmdStr)
var wg sync.WaitGroup
wg.Add(1)
stdout, err := command.StdoutPipe()
if err != nil {
return
}
readout := bufio.NewReader(stdout)
go func() {
defer wg.Done()
out = getOutput(readout)
}()
err = command.Run()
func Exec(cmdStr string) (string, error) {
cmd := exec.Command("bash", "-c", cmdStr)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return
}
wg.Wait()
return
}
func getOutput(reader *bufio.Reader) string {
var sumOutput string
outputBytes := make([]byte, 200)
for {
n, err := reader.Read(outputBytes)
if err != nil {
if err == io.EOF {
break
}
sumOutput += err.Error()
}
output := string(outputBytes[:n])
sumOutput += output
return string(stderr.Bytes()), err
}
return sumOutput
return string(stdout.Bytes()), nil
}

23
backend/utils/nginx/components/block.go

@ -28,9 +28,9 @@ func (b *Block) FindDirectives(directiveName string) []IDirective {
}
func (b *Block) UpdateDirectives(directiveName string, directive Directive) {
directives := make([]IDirective, len(b.GetDirectives()))
directives := b.GetDirectives()
index := -1
for i, dir := range b.GetDirectives() {
for i, dir := range directives {
if dir.GetName() == directiveName {
index = i
break
@ -44,6 +44,25 @@ func (b *Block) UpdateDirectives(directiveName string, directive Directive) {
b.Directives = directives
}
func (b *Block) UpdateDirectiveBySecondKey(name string, key string, directive Directive) {
directives := b.GetDirectives()
index := -1
for i, dir := range directives {
if dir.GetName() == name && dir.GetParameters()[0] == key {
index = i
break
}
}
if index > -1 {
directives[index] = &directive
} else {
directives = append(directives, &directive)
}
b.Directives = directives
}
func (b *Block) AddDirectives(directive Directive) {
directives := append(b.GetDirectives(), &directive)
b.Directives = directives

36
backend/utils/nginx/components/server.go

@ -144,11 +144,28 @@ func (s *Server) UpdateRootProxy(proxy []string) {
Parameters: proxy,
})
newDir.Block = block
s.UpdateDirectives("location", newDir)
s.UpdateDirectiveBySecondKey("location", "/", newDir)
}
}
}
func (s *Server) UpdateDirectiveBySecondKey(name string, key string, directive Directive) {
directives := s.Directives
index := -1
for i, dir := range directives {
if dir.GetName() == name && dir.GetParameters()[0] == key {
index = i
break
}
}
if index > -1 {
directives[index] = &directive
} else {
directives = append(directives, &directive)
}
s.Directives = directives
}
func (s *Server) RemoveListenByBind(bind string) {
index := 0
listens := s.Listens
@ -176,14 +193,19 @@ func (s *Server) FindDirectives(directiveName string) []IDirective {
}
func (s *Server) UpdateDirectives(directiveName string, directive Directive) {
directives := make([]IDirective, 0)
for _, dir := range s.Directives {
directives := s.Directives
index := -1
for i, dir := range directives {
if dir.GetName() == directiveName {
directives = append(directives, &directive)
} else {
directives = append(directives, dir)
index = i
break
}
}
if index > -1 {
directives[index] = &directive
} else {
directives = append(directives, &directive)
}
s.Directives = directives
}
@ -197,7 +219,7 @@ func (s *Server) RemoveDirectives(names []string) {
for _, name := range names {
nameMaps[name] = struct{}{}
}
directives := s.GetDirectives()
directives := s.Directives
var newDirectives []IDirective
for _, dir := range directives {
if _, ok := nameMaps[dir.GetName()]; ok {

5
cmd/server/nginx_conf/limit.conf

@ -1,3 +1,2 @@
limit_conn perserver 300;
limit_conn perip 25;
limit_rate 512k;
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;

3
cmd/server/nginx_conf/nginx_conf.go

@ -12,3 +12,6 @@ var HTTPS []byte
//go:embed website_default.conf
var WebsiteDefault []byte
//go:embed limit.conf
var Limit []byte

8
frontend/src/api/interface/website.ts

@ -65,8 +65,16 @@ export namespace WebSite {
}
export interface NginxConfigReq {
operate: string;
websiteId: number;
scope: string;
params?: any;
}
export interface NginxParam {
name: string;
secondKey: string;
isRepeatKey: string;
params: string[];
}
}

2
frontend/src/api/modules/website.ts

@ -43,7 +43,7 @@ export const CreateDomain = (req: WebSite.DomainCreate) => {
};
export const GetNginxConfig = (req: WebSite.NginxConfigReq) => {
return http.post<any>(`/websites/config`, req);
return http.post<WebSite.NginxParam[]>(`/websites/config`, req);
};
export const UpdateNginxConfig = (req: WebSite.NginxConfigReq) => {

7
frontend/src/lang/modules/zh.ts

@ -687,5 +687,12 @@ export default {
addDomain: '新增域名',
domainConfig: '域名设置',
defaultDoc: '默认文档',
perserver: '并发限制',
perserverHelper: '限制当前站点最大并发数',
perip: '单IP限制',
peripHelper: '限制单个IP访问最大并发数',
rate: '流量限制',
rateHelper: '限制每个请求的流量上限单位KB',
limtHelper: '启用流量控制',
},
};

33
frontend/src/views/website/project/config/basic/default-doc/index.vue

@ -1,23 +1,20 @@
<template>
<div>
<el-form ref="defaultForm" label-position="top" :model="defaultModel" :rules="rules" :loading="loading">
<el-form-item :label="$t('website.domain')" prop="index">
<el-col :span="8">
<el-row :gutter="20">
<el-col :span="8" :offset="2">
<el-form ref="defaultForm" label-position="top" :model="defaultModel" :rules="rules" :loading="loading">
<el-form-item :label="$t('website.defaultDoc')" prop="index">
<el-input
width="40%"
v-model="defaultModel.index"
type="textarea"
:autosize="{ minRows: 8, maxRows: 20 }"
></el-input>
</el-col>
</el-form-item>
</el-form>
<span class="dialog-footer">
</el-form-item>
</el-form>
<el-button type="primary" @click="submit(defaultForm)" :loading="loading">
{{ $t('commons.button.save') }}
</el-button>
</span>
</div>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
@ -45,6 +42,7 @@ let defaultModel = ref({
index: '',
});
let req = ref({
operate: 'update',
scope: 'index',
websiteId: websiteId.value,
params: {},
@ -75,13 +73,14 @@ const search = (req: WebSite.NginxConfigReq) => {
loading.value = true;
GetNginxConfig(req)
.then((res) => {
const params = res.data['index'];
let values = '';
for (const param of params) {
values = values + param + '\n';
if (res.data.length > 0) {
const indexParam = res.data[0];
let values = '';
for (const param of indexParam.params) {
values = values + param + '\n';
}
defaultModel.value.index = values;
}
defaultModel.value.index = values;
})
.finally(() => {
loading.value = false;

6
frontend/src/views/website/project/config/basic/index.vue

@ -6,11 +6,14 @@
<el-tab-pane :label="$t('website.defaultDoc')">
<Default :id="id" v-if="index == '1'"></Default>
</el-tab-pane>
<el-tab-pane :label="$t('website.rate')">
<LimitConn :id="id" v-if="index == '2'"></LimitConn>
</el-tab-pane>
<el-tab-pane label="HTTPS">Role</el-tab-pane>
<el-tab-pane label="域名跳转">Task</el-tab-pane>
<el-tab-pane label="错误页面">Task</el-tab-pane>
<el-tab-pane label="过期时间">Task</el-tab-pane>
<el-tab-pane label="流量限制">Task</el-tab-pane>
<el-tab-pane label="默认文档">Task</el-tab-pane>
</el-tabs>
</template>
@ -20,6 +23,7 @@ import { computed, ref } from 'vue';
import Doamin from './domain/index.vue';
import Default from './default-doc/index.vue';
import LimitConn from './limit-conn/index.vue';
const props = defineProps({
id: {

135
frontend/src/views/website/project/config/basic/limit-conn/index.vue

@ -0,0 +1,135 @@
<template>
<el-row :gutter="20">
<el-col :span="8" :offset="2">
<el-checkbox v-model="enable" @change="changeEnable">{{ $t('website.limtHelper') }}</el-checkbox>
<el-form ref="limitForm" label-position="left" :model="form" :rules="rules" :loading="loading">
<el-form-item :label="$t('website.perserver')" prop="perserver">
<el-input v-model="form.perserver"></el-input>
<span class="input-help">{{ $t('website.perserverHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('website.perip')" prop="perip">
<el-input v-model="form.perip"></el-input>
<span class="input-help">{{ $t('website.peripHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('website.rate')" prop="rate">
<el-input v-model="form.rate"></el-input>
<span class="input-help">{{ $t('website.rateHelper') }}</span>
</el-form-item>
</el-form>
<el-button type="primary" @click="submit(limitForm)" :loading="loading">
{{ $t('commons.button.save') }}
</el-button>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { Rules } from '@/global/form-rules';
import { WebSite } from '@/api/interface/website';
import { GetNginxConfig, UpdateNginxConfig } from '@/api/modules/website';
import { ElMessage, FormInstance } from 'element-plus';
import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const websiteId = computed(() => {
return Number(props.id);
});
let rules = ref({
perserver: [Rules.requiredInput],
perip: [Rules.requiredInput],
rate: [Rules.requiredInput],
});
const limitForm = ref<FormInstance>();
let form = reactive({
perserver: 300,
perip: 25,
rate: 512,
});
let req = reactive({
operate: 'update',
scope: 'limit-conn',
websiteId: websiteId.value,
params: [{}],
});
let enable = ref(false);
let loading = ref(false);
const search = (req: WebSite.NginxConfigReq) => {
loading.value = true;
GetNginxConfig(req)
.then((res) => {
if (res.data.length > 0) {
enable.value = true;
for (const param of res.data) {
if (param.name === 'limit_conn') {
if (param.secondKey === 'perserver') {
form.perserver = Number(param.params[1]);
}
if (param.secondKey === 'perip') {
form.perip = Number(param.params[1]);
}
}
if (param.name === 'limit_rate') {
console.log(param.params[0].match(/\d+/g)?.join(''));
form.rate = Number(param.params[0].match(/\d+/g));
}
}
} else {
enable.value = false;
req.operate = 'add';
}
})
.finally(() => {
loading.value = false;
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
let params = [
{
limit_conn: 'perserver ' + String(form.perserver),
},
{
limit_conn: 'perip ' + String(form.perip),
},
{
limit_rate: String(form.rate) + 'k',
},
];
req.params = params;
UpdateNginxConfig(req)
.then(() => {
ElMessage.success(i18n.global.t('commons.msg.updateSuccess'));
search(req);
})
.finally(() => {
loading.value = false;
});
});
};
const changeEnable = () => {
if (!enable.value) {
req.operate = 'delete';
} else {
req.operate = 'add';
}
submit(limitForm.value);
};
onMounted(() => {
search(req);
});
</script>
Loading…
Cancel
Save