feat: 增加流量限制功能

pull/35/head
zhengkunwang223 2 years ago committed by zhengkunwang223
parent daa2e12420
commit d5aee147f4

@ -10,17 +10,41 @@ type NginxConfig struct {
} }
type NginxConfigReq struct { type NginxConfigReq struct {
Scope NginxScope `json:"scope"` Scope NginxScope `json:"scope"`
WebSiteID uint `json:"webSiteId" validate:"required"` Operate NginxOp `json:"operate"`
Params map[string]string `json:"params"` WebSiteID uint `json:"webSiteId" validate:"required"`
Params interface{} `json:"params"`
} }
type NginxScope string type NginxScope string
const ( 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{ 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"`
} }

@ -186,7 +186,7 @@ func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
return websiteDomainRepo.DeleteBy(context.TODO(), commonRepo.WithByID(domainId)) 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] keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 { if !ok || len(keys) == 0 {
@ -207,19 +207,13 @@ func (w WebsiteService) UpdateNginxConfigByScope(req dto.NginxConfigReq) error {
if !ok || len(keys) == 0 { if !ok || len(keys) == 0 {
return nil 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)) website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebSiteID))
if err != nil { if err != nil {
return err 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)
} }

@ -91,8 +91,8 @@ func opNginx(containerName, operate string) error {
if operate == "check" { if operate == "check" {
nginxCmd = fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -t") nginxCmd = fmt.Sprintf("docker exec -i %s %s", containerName, "nginx -t")
} }
if _, err := cmd.Exec(nginxCmd); err != nil { if out, err := cmd.Exec(nginxCmd); err != nil {
return err return errors.New(out)
} }
return nil return nil
} }
@ -206,36 +206,50 @@ func deleteListenAndServerName(website model.WebSite, ports []int, domains []str
return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxConfig.ContainerName) 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) nginxConfig, err := getNginxConfig(website.PrimaryDomain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
config := nginxConfig.Config config := nginxConfig.Config
server := config.FindServers()[0] server := config.FindServers()[0]
res := make(map[string]interface{})
var res []dto.NginxParam
for _, key := range keys { for _, key := range keys {
dirs := server.FindDirectives(key) dirs := server.FindDirectives(key)
for _, dir := range dirs { 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 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) nginxConfig, err := getNginxConfig(website.PrimaryDomain)
if err != nil { if err != nil {
return err return err
} }
config := nginxConfig.Config config := nginxConfig.Config
updateConfig(config, scope)
server := config.FindServers()[0] server := config.FindServers()[0]
for k, v := range keyValues { for _, p := range params {
newDir := components.Directive{ newDir := components.Directive{
Name: k, Name: p.Name,
Parameters: v, 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 { if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err return err
@ -243,13 +257,101 @@ func updateNginxConfig(website model.WebSite, keyValues map[string][]string) err
return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxConfig.ContainerName) 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 var res []string
switch param.(type) { switch param.(type) {
case string: case string:
if key == "index" { if key == "index" {
res = strings.Split(param.(string), "\n") res = strings.Split(param.(string), "\n")
return res
} }
res = strings.Split(param.(string), " ")
return res
} }
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
}

@ -1,50 +1,18 @@
package cmd package cmd
import ( import (
"bufio" "bytes"
"context"
"io"
"os/exec" "os/exec"
"sync"
) )
func Exec(cmdStr string) (out string, err error) { func Exec(cmdStr string) (string, error) {
command := exec.CommandContext(context.Background(), "bash", "-c", cmdStr) cmd := exec.Command("bash", "-c", cmdStr)
var stdout, stderr bytes.Buffer
var wg sync.WaitGroup cmd.Stdout = &stdout
wg.Add(1) cmd.Stderr = &stderr
err := cmd.Run()
stdout, err := command.StdoutPipe()
if err != nil {
return
}
readout := bufio.NewReader(stdout)
go func() {
defer wg.Done()
out = getOutput(readout)
}()
err = command.Run()
if err != nil { if err != nil {
return return string(stderr.Bytes()), err
}
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 sumOutput return string(stdout.Bytes()), nil
} }

@ -28,9 +28,9 @@ func (b *Block) FindDirectives(directiveName string) []IDirective {
} }
func (b *Block) UpdateDirectives(directiveName string, directive Directive) { func (b *Block) UpdateDirectives(directiveName string, directive Directive) {
directives := make([]IDirective, len(b.GetDirectives())) directives := b.GetDirectives()
index := -1 index := -1
for i, dir := range b.GetDirectives() { for i, dir := range directives {
if dir.GetName() == directiveName { if dir.GetName() == directiveName {
index = i index = i
break break
@ -44,6 +44,25 @@ func (b *Block) UpdateDirectives(directiveName string, directive Directive) {
b.Directives = directives 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) { func (b *Block) AddDirectives(directive Directive) {
directives := append(b.GetDirectives(), &directive) directives := append(b.GetDirectives(), &directive)
b.Directives = directives b.Directives = directives

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

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

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

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

@ -43,7 +43,7 @@ export const CreateDomain = (req: WebSite.DomainCreate) => {
}; };
export const GetNginxConfig = (req: WebSite.NginxConfigReq) => { 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) => { export const UpdateNginxConfig = (req: WebSite.NginxConfigReq) => {

@ -687,5 +687,12 @@ export default {
addDomain: '', addDomain: '',
domainConfig: '', domainConfig: '',
defaultDoc: '', defaultDoc: '',
perserver: '',
perserverHelper: '',
perip: 'IP',
peripHelper: 'IP访',
rate: '',
rateHelper: 'KB',
limtHelper: '',
}, },
}; };

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

@ -6,11 +6,14 @@
<el-tab-pane :label="$t('website.defaultDoc')"> <el-tab-pane :label="$t('website.defaultDoc')">
<Default :id="id" v-if="index == '1'"></Default> <Default :id="id" v-if="index == '1'"></Default>
</el-tab-pane> </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="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-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> </el-tabs>
</template> </template>
@ -20,6 +23,7 @@ import { computed, ref } from 'vue';
import Doamin from './domain/index.vue'; import Doamin from './domain/index.vue';
import Default from './default-doc/index.vue'; import Default from './default-doc/index.vue';
import LimitConn from './limit-conn/index.vue';
const props = defineProps({ const props = defineProps({
id: { id: {

@ -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