1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 14:08:06 +08:00

feat: 反向代理增加编辑源文功能

This commit is contained in:
zhengkunwang223 2023-04-24 17:48:22 +08:00 committed by zhengkunwang223
parent 2dd88364f8
commit acf64dcf25
14 changed files with 355 additions and 25 deletions

View File

@ -679,6 +679,7 @@ func (b *BaseApi) GetProxyConfig(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/proxies/update [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"websites","output_colume":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 反向代理配置 ","formatEN":"Update domain [domain] proxy config"}
func (b *BaseApi) UpdateProxyConfig(c *gin.Context) {
var req request.WebsiteProxyConfig
if err := c.ShouldBindJSON(&req); err != nil {
@ -690,5 +691,27 @@ func (b *BaseApi) UpdateProxyConfig(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Update proxy file
// @Description 更新反向代理文件
// @Accept json
// @Param request body request.NginxProxyUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/proxy/file [post]
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"websiteID","isList":false,"db":"websites","output_colume":"primary_domain","output_value":"domain"}],"formatZH":"更新反向代理文件 [domain]","formatEN":"Nginx conf proxy file update [domain]"}
func (b *BaseApi) UpdateProxyConfigFile(c *gin.Context) {
var req request.NginxProxyUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := websiteService.UpdateProxyFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -30,3 +30,9 @@ type NginxRewriteUpdate struct {
Name string `json:"name" validate:"required"`
Content string `json:"content" validate:"required"`
}
type NginxProxyUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Content string `json:"content" validate:"required"`
Name string `json:"name" validate:"required"`
}

View File

@ -158,19 +158,20 @@ type WebsiteUpdateDirPermission struct {
}
type WebsiteProxyConfig struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
Enable bool `json:"enable" validate:"required"`
Cache bool `json:"cache" validate:"required"`
CacheTime int `json:"cacheTime" validate:"required"`
CacheUnit string `json:"cacheUnit" validate:"required"`
Name string `json:"name" validate:"required"`
Modifier string `json:"modifier" validate:"required"`
Match string `json:"match" validate:"required"`
ProxyPass string `json:"proxyPass" validate:"required"`
ProxyHost string `json:"proxyHost" validate:"required"`
FilePath string `json:"filePath"`
Replaces []map[string]string `json:"replaces"`
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
Enable bool `json:"enable" validate:"required"`
Cache bool `json:"cache" validate:"required"`
CacheTime int `json:"cacheTime" validate:"required"`
CacheUnit string `json:"cacheUnit" validate:"required"`
Name string `json:"name" validate:"required"`
Modifier string `json:"modifier" validate:"required"`
Match string `json:"match" validate:"required"`
ProxyPass string `json:"proxyPass" validate:"required"`
ProxyHost string `json:"proxyHost" validate:"required"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Replaces map[string]string `json:"replaces"`
}
type WebsiteProxyReq struct {

View File

@ -69,6 +69,7 @@ type IWebsiteService interface {
UpdateSitePermission(req request.WebsiteUpdateDirPermission) error
OperateProxy(req request.WebsiteProxyConfig) (err error)
GetProxies(id uint) (res []request.WebsiteProxyConfig, err error)
UpdateProxyFile(req request.NginxProxyUpdate) (err error)
}
func NewIWebsiteService() IWebsiteService {
@ -1213,6 +1214,11 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
} else {
location.RemoveCache()
}
if len(req.Replaces) > 0 {
location.AddSubFilter(req.Replaces)
} else {
location.RemoveSubFilter()
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
}
@ -1266,6 +1272,7 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
if err != nil {
return
}
proxyConfig.Content = string(content)
config = parser.NewStringParser(string(content)).Parse()
directives := config.GetDirectives()
@ -1283,7 +1290,40 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
proxyConfig.Match = location.Match
proxyConfig.Modifier = location.Modifier
proxyConfig.ProxyHost = location.Host
proxyConfig.Replaces = location.Replaces
res = append(res, proxyConfig)
}
return
}
func (w WebsiteService) UpdateProxyFile(req request.NginxProxyUpdate) (err error) {
var (
website model.Website
nginxFull dto.NginxFull
oldRewriteContent []byte
)
website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxFull, err = getNginxFull(&website)
if err != nil {
return err
}
includePath := fmt.Sprintf("/www/sites/%s/proxy/%s.conf", website.Alias, req.Name)
absolutePath := path.Join(nginxFull.Install.GetPath(), includePath)
fileOp := files.NewFileOp()
oldRewriteContent, err = fileOp.GetContent(absolutePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil {
return err
}
defer func() {
if err != nil {
_ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755)
}
}()
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}

View File

@ -54,5 +54,6 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) {
groupRouter.POST("/proxies", baseApi.GetProxyConfig)
groupRouter.POST("/proxies/update", baseApi.UpdateProxyConfig)
groupRouter.POST("/proxies/file", baseApi.UpdateProxyConfigFile)
}
}

View File

@ -46,6 +46,7 @@ var repeatKeys = map[string]struct {
"proxy_set_header": {},
"location": {},
"include": {},
"sub_filter": {},
}
func IsRepeatKey(key string) bool {

View File

@ -1,8 +1,10 @@
package components
import (
"fmt"
"regexp"
"strconv"
"strings"
)
type Location struct {
@ -17,6 +19,7 @@ type Location struct {
Directives []IDirective
Line int
Parameters []string
Replaces map[string]string
}
func NewLocation(directive IDirective) *Location {
@ -60,6 +63,11 @@ func NewLocation(directive IDirective) *Location {
}
}
}
case "sub_filter":
if location.Replaces == nil {
location.Replaces = make(map[string]string, 0)
}
location.Replaces[strings.Trim(params[0], "\"")] = strings.Trim(params[1], "\"")
}
}
@ -176,6 +184,7 @@ func (l *Location) ChangePath(Modifier string, Match string) {
func (l *Location) AddCache(cacheTime int, cacheUint string) {
l.RemoveDirective("add_header", []string{"Cache-Control", "no-cache"})
l.RemoveDirective("if", []string{"(", "$uri", "~*", `"\.(gif|png|jpg|css|js|woff|woff2)$"`, ")"})
directives := l.GetDirectives()
newDir := &Directive{
Name: "if",
@ -212,3 +221,20 @@ func (l *Location) RemoveCache() {
l.CacheUint = ""
l.Cache = false
}
func (l *Location) AddSubFilter(subFilters map[string]string) {
l.RemoveDirective("sub_filter", []string{})
l.Replaces = subFilters
for k, v := range subFilters {
l.UpdateDirective("sub_filter", []string{fmt.Sprintf(`"%s"`, k), fmt.Sprintf(`"%s"`, v)})
}
l.UpdateDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
l.UpdateDirective("sub_filter_once", []string{"off"})
}
func (l *Location) RemoveSubFilter() {
l.RemoveDirective("sub_filter", []string{})
l.RemoveDirective("proxy_set_header", []string{"Accept-Encoding", `""`})
l.RemoveDirective("sub_filter_once", []string{"off"})
l.Replaces = nil
}

View File

@ -326,9 +326,16 @@ export namespace Website {
proxyHost: string;
filePath?: string;
replaces?: ProxReplace;
content?: string;
}
interface ProxReplace {
export interface ProxReplace {
[key: string]: string;
}
export interface ProxyFileUpdate {
websiteID: number;
name: string;
content: string;
}
}

View File

@ -191,6 +191,10 @@ export const GetProxyConfig = (req: Website.ProxyReq) => {
return http.post<Website.ProxyConfig[]>(`/websites/proxies`, req);
};
export const CreateProxyConfig = (req: Website.ProxyReq) => {
export const OperateProxyConfig = (req: Website.ProxyReq) => {
return http.post<any>(`/websites/proxies/update`, req);
};
export const UpdateProxyConfigFile = (req: Website.ProxyFileUpdate) => {
return http.post<any>(`/websites/proxies/file`, req);
};

View File

@ -1193,6 +1193,24 @@ const message = {
disabled: 'Stopped',
startProxy: 'Start Reverse proxy',
stopProxy: 'Stop the Reverse proxy',
proxyFile: 'Source',
proxyHelper1:
'Proxy directory: when accessing this directory, the content of the target URL will be returned and displayed',
proxyPassHelper:
'Target URL: You can fill in the site you need to proxy, the target URL must be a URL that can be accessed normally, otherwise an error will be returned',
proxyHostHelper:
'Send domain name: Add the domain name to the request header and pass it to the proxy server. The default is the target URL domain name. If it is not set properly, the proxy may not work properly',
replacementHelper:
'Content replacement: you can add up to 3 replacement content, if you do not need to replace, please leave blank',
modifier: 'Path Match',
modifierHelper:
'Path matching: Example: = exact match, ~ regular match, ^~ match the beginning of the path, etc',
replace: 'Content Replacement',
addReplace: 'Add content to replace',
replaced: 'The replaced text cannot be empty',
replaceText: 'Replacement text, can be empty',
replacedErr: 'The replaced text cannot be empty',
replacedErr2: 'The replaced text cannot be repeated',
},
php: {
short_open_tag: 'Short tag support',

View File

@ -1195,6 +1195,20 @@ const message = {
disabled: '已停止',
startProxy: '开启反向代理',
stopProxy: '关闭反向代理',
proxyFile: '源文',
proxyHelper1: '代理目录访问这个目录时将会把目标URL的内容返回并显示',
proxyPassHelper: '目标URL可以填写你需要代理的站点目标URL必须为可正常访问的URL否则将返回错误',
proxyHostHelper:
'发送域名将域名添加到请求头传递到代理服务器默认为目标URL域名若设置不当可能导致代理无法正常运行',
replacementHelper: '内容替换最多可以添加3条替换内容,如果不需要替换请留空',
modifier: '路径匹配',
modifierHelper: '路径匹配= 精确匹配~ 正则匹配^~ 匹配路径开头 ',
replace: '内容替换',
addReplace: '添加内容替换',
replaced: '被替换的文本,不能为空',
replaceText: '替换的文本可为空',
replacedErr: '被替换的文本不能为空',
replacedErr2: '被替换的文本不能重复',
},
php: {
short_open_tag: '短标签支持',

View File

@ -9,6 +9,9 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="proxy.name" :disabled="proxy.operate === 'edit'"></el-input>
</el-form-item>
<el-form-item :label="$t('website.modifier')" prop="modifier">
<el-input v-model.trim="proxy.modifier"></el-input>
</el-form-item>
<el-form-item :label="$t('website.proxyPath')" prop="match">
<el-input v-model.trim="proxy.match"></el-input>
</el-form-item>
@ -41,7 +44,40 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('website.replace')">
<div style="width: 100%" v-for="(replace, index) in replaces" :key="index">
<el-row :gutter="10">
<el-col :span="10">
<el-input
v-model.trim="replace.key"
:placeholder="$t('website.replaced')"
></el-input>
</el-col>
<el-col :span="10">
<el-input
v-model.trim="replace.value"
:placeholder="$t('website.replaceText')"
></el-input>
</el-col>
<el-col :span="2">
<el-button link @click="removeReplace(index)" type="danger">
{{ $t('commons.button.delete') }}
</el-button>
</el-col>
</el-row>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addReplaces" :disabled="replaces.length >= 3">
{{ $t('website.addReplace') }}
</el-button>
</el-form-item>
</el-form>
<el-alert :title="$t('website.modifierHelper')" type="info" :closable="false" />
<el-alert :title="$t('website.proxyHelper1')" type="info" :closable="false" />
<el-alert :title="$t('website.proxyPassHelper')" type="info" :closable="false" />
<el-alert :title="$t('website.proxyHostHelper')" type="info" :closable="false" />
<el-alert :title="$t('website.replacementHelper')" type="info" :closable="false" />
</el-col>
</el-row>
<template #footer>
@ -57,12 +93,12 @@
<script lang="ts" setup>
import DrawerHeader from '@/components/drawer-header/index.vue';
import { CreateProxyConfig } from '@/api/modules/website';
import { OperateProxyConfig } from '@/api/modules/website';
import { checkNumberRange, Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Website } from '@/api/interface/website';
import { Units } from '@/global/mimetype';
@ -70,6 +106,7 @@ const proxyForm = ref<FormInstance>();
const rules = ref({
name: [Rules.requiredInput, Rules.appName],
match: [Rules.requiredInput],
modifier: [Rules.requiredInput],
cacheTime: [Rules.requiredInput, checkNumberRange(1, 65535)],
proxyPass: [Rules.requiredInput],
proxyHost: [Rules.requiredInput],
@ -90,9 +127,10 @@ const initData = (): Website.ProxyConfig => ({
proxyPass: 'http://',
proxyHost: '$host',
filePath: '',
replaces: {},
});
let proxy = ref(initData());
const replaces = ref<any>([]);
const em = defineEmits(['close']);
const handleClose = () => {
proxyForm.value?.resetFields();
@ -100,10 +138,15 @@ const handleClose = () => {
em('close', false);
};
const acceptParams = async (proxyParam: Website.ProxyConfig) => {
const acceptParams = (proxyParam: Website.ProxyConfig) => {
replaces.value = [];
proxy.value = proxyParam;
console.log(proxy.value);
open.value = true;
if (proxy.value.replaces) {
for (const key in proxy.value.replaces) {
replaces.value.push({ key: key, value: proxy.value.replaces[key] });
}
}
};
const changeCache = (cache: boolean) => {
@ -116,14 +159,39 @@ const changeCache = (cache: boolean) => {
}
};
const addReplaces = () => {
replaces.value.push({ key: '', value: '' });
};
const removeReplace = (index: number) => {
replaces.value.splice(index, 1);
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
proxy.value.replaces = {};
if (replaces.value.length > 0) {
let keyMap = new Map();
for (const rep of replaces.value) {
if (keyMap.get(rep.key) != undefined) {
MsgError(i18n.global.t('website.replacedErr2'));
return;
}
keyMap.set(rep.key, '');
if (rep.key === '') {
MsgError(i18n.global.t('website.replacedErr'));
return;
}
proxy.value.replaces[rep.key] = rep.value;
}
}
loading.value = true;
CreateProxyConfig(proxy.value)
OperateProxyConfig(proxy.value)
.then(() => {
if (proxy.value.operate == 'create') {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));

View File

@ -0,0 +1,84 @@
<template>
<el-drawer v-model="open" :close-on-click-modal="false" size="40%" :before-close="handleClose">
<template #header>
<DrawerHeader :header="$t('website.proxyFile')" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<codemirror
:autofocus="true"
placeholder=""
:indent-with-tab="true"
:tabSize="4"
style="margin-top: 10px; height: 600px; width: 100%"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="req.content"
/>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import DrawerHeader from '@/components/drawer-header/index.vue';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { reactive, ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Codemirror } from 'vue-codemirror';
import { UpdateProxyConfigFile } from '@/api/modules/website';
import { StreamLanguage } from '@codemirror/language';
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
import { oneDark } from '@codemirror/theme-one-dark';
const extensions = [StreamLanguage.define(nginx), oneDark];
const proxyForm = ref<FormInstance>();
const open = ref(false);
const loading = ref(false);
const em = defineEmits(['close']);
const handleClose = () => {
proxyForm.value?.resetFields();
open.value = false;
em('close', false);
};
const req = reactive({
name: '',
websiteID: 0,
content: '',
});
const acceptParams = async (proxyreq: any) => {
req.name = proxyreq.name;
req.websiteID = proxyreq.websiteID;
req.content = proxyreq.content;
open.value = true;
};
const submit = async () => {
loading.value = true;
UpdateProxyConfigFile(req)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -8,7 +8,7 @@
<el-table-column :label="$t('website.proxyPass')" prop="proxyPass"></el-table-column>
<el-table-column :label="$t('website.cache')" prop="cache">
<template #default="{ row }">
<el-switch v-model="row.cache" @change="changeCache(row)"></el-switch>
<el-switch v-model="row.cache" @change="changeCache(row)" :disabled="!row.enable"></el-switch>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="enable">
@ -31,16 +31,20 @@
/>
</ComplexTable>
<Create ref="createRef" @close="search()" />
<File ref="fileRef" @close="search()" />
</template>
<script lang="ts" setup name="proxy">
import { Website } from '@/api/interface/website';
import { CreateProxyConfig, GetProxyConfig } from '@/api/modules/website';
import { OperateProxyConfig, GetProxyConfig } from '@/api/modules/website';
import { computed, onMounted, ref } from 'vue';
import Create from './create/index.vue';
import File from './file/index.vue';
import { VideoPlay, VideoPause } from '@element-plus/icons-vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { useDeleteData } from '@/hooks/use-delete-data';
import { ElMessageBox } from 'element-plus';
const props = defineProps({
id: {
@ -54,13 +58,32 @@ const id = computed(() => {
const loading = ref(false);
const data = ref();
const createRef = ref();
const fileRef = ref();
const buttons = [
{
label: i18n.global.t('website.proxyFile'),
click: function (row: Website.ProxyConfig) {
openEditFile(row);
},
disabled: (row: Website.ProxyConfig) => {
return !row.enable;
},
},
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Website.ProxyConfig) {
openEdit(row);
},
disabled: (row: Website.ProxyConfig) => {
return !row.enable;
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Website.ProxyConfig) {
deleteProxy(row);
},
},
];
@ -76,6 +99,7 @@ const initData = (id: number): Website.ProxyConfig => ({
match: '/',
proxyPass: 'http://',
proxyHost: '$host',
replaces: {},
});
const openCreate = () => {
@ -85,9 +109,22 @@ const openCreate = () => {
const openEdit = (proxyConfig: Website.ProxyConfig) => {
let proxy = JSON.parse(JSON.stringify(proxyConfig));
proxy.operate = 'edit';
if (proxy.replaces == null) {
proxy.replaces = {};
}
createRef.value.acceptParams(proxy);
};
const openEditFile = (proxyConfig: Website.ProxyConfig) => {
fileRef.value.acceptParams({ name: proxyConfig.name, content: proxyConfig.content, websiteID: proxyConfig.id });
};
const deleteProxy = async (proxyConfig: Website.ProxyConfig) => {
proxyConfig.operate = 'delete';
await useDeleteData(OperateProxyConfig, proxyConfig, 'commons.msg.delete');
search();
};
const changeCache = (proxyConfig: Website.ProxyConfig) => {
proxyConfig.operate = 'edit';
if (proxyConfig.cache) {
@ -99,7 +136,7 @@ const changeCache = (proxyConfig: Website.ProxyConfig) => {
const submit = async (proxyConfig: Website.ProxyConfig) => {
loading.value = true;
CreateProxyConfig(proxyConfig)
OperateProxyConfig(proxyConfig)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();