diff --git a/main.go b/main.go index eb95f63..716cb45 100644 --- a/main.go +++ b/main.go @@ -122,6 +122,9 @@ func Run() error { if err := global.DB.AutoMigrate(&model.Asset{}); err != nil { return err } + if err := global.DB.AutoMigrate(&model.AssetAttribute{}); err != nil { + return err + } if err := global.DB.AutoMigrate(&model.Session{}); err != nil { return err } diff --git a/pkg/api/asset.go b/pkg/api/asset.go index 9032014..8c90f1b 100644 --- a/pkg/api/asset.go +++ b/pkg/api/asset.go @@ -106,6 +106,31 @@ func AssetUpdateEndpoint(c echo.Context) error { return Success(c, nil) } +func AssetGetAttributeEndpoint(c echo.Context) error { + + assetId := c.Param("id") + attributeMap, err := model.FindAssetAttrMapByAssetId(assetId) + if err != nil { + return err + } + return Success(c, attributeMap) +} + +func AssetUpdateAttributeEndpoint(c echo.Context) error { + m := echo.Map{} + if err := c.Bind(&m); err != nil { + return err + } + + assetId := c.Param("id") + protocol := c.QueryParam("protocol") + err := model.UpdateAssetAttributes(assetId, protocol, m) + if err != nil { + return err + } + return Success(c, "") +} + func AssetDeleteEndpoint(c echo.Context) error { id := c.Param("id") split := strings.Split(id, ",") diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 1936864..7a6f54d 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -74,6 +74,8 @@ func SetupRoutes() *echo.Echo { assets.GET("/paging", AssetPagingEndpoint) assets.POST("/:id/tcping", AssetTcpingEndpoint) assets.PUT("/:id", AssetUpdateEndpoint) + assets.GET("/:id/attributes", AssetGetAttributeEndpoint) + assets.PUT("/:id/attributes", AssetUpdateAttributeEndpoint) assets.DELETE("/:id", AssetDeleteEndpoint) assets.GET("/:id", AssetGetEndpoint) assets.POST("/:id/change-owner", Admin(AssetChangeOwnerEndpoint)) diff --git a/pkg/api/tunnel.go b/pkg/api/tunnel.go index 496d8ab..ddf1510 100644 --- a/pkg/api/tunnel.go +++ b/pkg/api/tunnel.go @@ -124,6 +124,15 @@ func TunEndpoint(c echo.Context) error { configuration.SetParameter("hostname", session.IP) configuration.SetParameter("port", strconv.Itoa(session.Port)) + + // 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置 + attributes, _ := model.FindAssetAttributeByAssetId(session.AssetId) + if len(attributes) > 0 { + for i := range attributes { + attribute := attributes[i] + configuration.SetParameter(attribute.Name, attribute.Value) + } + } } for name := range configuration.Parameters { // 替换数据库空格字符串占位符为真正的空格 diff --git a/pkg/guacd/guacd.go b/pkg/guacd/guacd.go index ef785b3..3c17f5e 100644 --- a/pkg/guacd/guacd.go +++ b/pkg/guacd/guacd.go @@ -15,9 +15,11 @@ const ( RecordingPath = "recording-path" CreateRecordingPath = "create-recording-path" - FontName = "font-name" - FontSize = "font-size" - ColorScheme = "color-scheme" + FontName = "font-name" + FontSize = "font-size" + ColorScheme = "color-scheme" + Backspace = "backspace" + TerminalType = "terminal-type" EnableDrive = "enable-drive" DriveName = "drive-name" @@ -31,6 +33,17 @@ const ( DisableBitmapCaching = "disable-bitmap-caching" DisableOffscreenCaching = "disable-offscreen-caching" DisableGlyphCaching = "disable-glyph-caching" + + ColorDepth = "color-depth" + Cursor = "cursor" + SwapRedBlue = "swap-red-blue" + DestHost = "dest-host" + DestPort = "dest-port" + + UsernameRegex = "username-regex" + PasswordRegex = "password-regex" + LoginSuccessRegex = "login-success-regex" + LoginFailureRegex = "login-failure-regex" ) const Delimiter = ';' diff --git a/pkg/model/asset-attribute.go b/pkg/model/asset-attribute.go index 8b53790..4458de0 100644 --- a/pkg/model/asset-attribute.go +++ b/pkg/model/asset-attribute.go @@ -1 +1,108 @@ package model + +import ( + "fmt" + "github.com/labstack/echo/v4" + "gorm.io/gorm" + "next-terminal/pkg/global" + "next-terminal/pkg/guacd" + "next-terminal/pkg/utils" +) + +type AssetAttribute struct { + Id string `gorm:"index" json:"id"` + AssetId string `gorm:"index" json:"assetId"` + Name string `gorm:"index" json:"name"` + Value string `json:"value"` +} + +func (r *AssetAttribute) TableName() string { + return "asset_attributes" +} + +var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType} +var RDPParameterNames = []string{guacd.EnableWallpaper, guacd.EnableTheming, guacd.EnableFontSmoothing, guacd.EnableFullWindowDrag, guacd.EnableDesktopComposition, guacd.EnableMenuAnimations, guacd.DisableBitmapCaching, guacd.DisableOffscreenCaching, guacd.DisableGlyphCaching} +var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort} +var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex} + +func UpdateAssetAttributes(assetId, protocol string, m echo.Map) error { + var data []AssetAttribute + var parameterNames []string + switch protocol { + case "ssh": + parameterNames = SSHParameterNames + case "rdp": + parameterNames = RDPParameterNames + case "vnc": + parameterNames = VNCParameterNames + case "telnet": + parameterNames = TelnetParameterNames + } + + for i := range parameterNames { + name := parameterNames[i] + if m[name] != nil && m[name] != "" { + data = append(data, genAttribute(assetId, name, m)) + } + } + + return global.DB.Transaction(func(tx *gorm.DB) error { + err := tx.Where("asset_id = ?", assetId).Delete(&AssetAttribute{}).Error + if err != nil { + return err + } + return tx.CreateInBatches(&data, len(data)).Error + }) +} + +func genAttribute(assetId, name string, m echo.Map) AssetAttribute { + value := fmt.Sprintf("%v", m[name]) + attribute := AssetAttribute{ + Id: utils.Sign([]string{assetId, name}), + AssetId: assetId, + Name: name, + Value: value, + } + return attribute +} + +func FindAssetAttributeByAssetId(assetId string) (o []AssetAttribute, err error) { + err = global.DB.Where("asset_id = ?", assetId).Find(&o).Error + if o == nil { + o = make([]AssetAttribute, 0) + } + return o, err +} + +func FindAssetAttrMapByAssetId(assetId string) (map[string]interface{}, error) { + asset, err := FindAssetById(assetId) + if err != nil { + return nil, err + } + attributes, err := FindAssetAttributeByAssetId(assetId) + if err != nil { + return nil, err + } + + var parameterNames []string + switch asset.Protocol { + case "ssh": + parameterNames = SSHParameterNames + case "rdp": + parameterNames = RDPParameterNames + case "vnc": + parameterNames = VNCParameterNames + } + propertiesMap := FindAllPropertiesMap() + var attributeMap = make(map[string]interface{}) + for name := range propertiesMap { + if utils.Contains(parameterNames, name) { + attributeMap[name] = propertiesMap[name] + } + } + + for i := range attributes { + attributeMap[attributes[i].Name] = attributes[i].Value + } + return attributeMap, nil +} diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js index 499de28..707db36 100644 --- a/web/src/components/asset/Asset.js +++ b/web/src/components/asset/Asset.js @@ -40,6 +40,10 @@ import { import {PROTOCOL_COLORS} from "../../common/constants"; import Logout from "../user/Logout"; import {hasPermission, isAdmin} from "../../service/permission"; +import AssetSSHAttributeModal from "./AssetSSHAttributeModal"; +import AssetRDPAttributeModal from "./AssetRDPAttributeModal"; +import AssetVNCAttributeModal from "./AssetVNCAttributeModal"; +import AssetTelnetAttributeModal from "./AssetTelnetAttributeModal"; const confirm = Modal.confirm; const {Search} = Input; @@ -85,6 +89,8 @@ class Asset extends Component { users: [], selected: {}, selectedSharers: [], + attrVisible: false, + attributes: {} }; async componentDidMount() { @@ -224,6 +230,34 @@ class Asset extends Component { await this.showModal('复制资产', result.data); } + async attr(record) { + let result = await request.get(`/assets/${record['id']}/attributes`); + if (result.code !== 1) { + message.error(result.message, 10); + return; + } + + // eslint-disable-next-line no-extend-native + String.prototype.bool = function () { + return (/^true$/i).test(this); + }; + + let attributes = result['data']; + for (const key in attributes) { + if (!attributes.hasOwnProperty(key)) { + continue; + } + if (key === 'swap-red-blue') { + attributes[key] = attributes[key].bool(); + } + } + this.setState({ + selected: record, + attrVisible: true, + attributes: attributes + }); + } + async showModal(title, asset = {}) { // 并行请求 let getCredentials = request.get('/credentials'); @@ -413,6 +447,38 @@ class Asset extends Component { }) } + handleUpdateAttr = async (formData) => { + // 弹窗 form 传来的数据 + this.setState({ + modalConfirmLoading: true + }); + + try { + let selected = this.state.selected; + let result = await request.put(`/assets/${selected['id']}/attributes?protocol=${selected['protocol']}`, formData); + if (result['code'] !== 1) { + message.error(result['message'], 10); + } else { + message.success('操作成功'); + this.setState({ + attrVisible: false + }) + } + } finally { + this.setState({ + modalConfirmLoading: false + }); + } + } + + handleCancelUpdateAttr = () => { + this.setState({ + attrVisible: false, + selected: {}, + attributes: {} + }); + } + render() { const columns = [{ @@ -484,8 +550,14 @@ class Asset extends Component { onClick={() => this.copy(record.id)}>复制 + + + + {isAdmin() ? - + @@ -692,7 +764,6 @@ class Asset extends Component { { this.state.modalVisible ? : undefined } + { + this.state.attrVisible && this.state.selected['protocol'] === 'ssh' ? + + : null + } + + { + this.state.attrVisible && this.state.selected['protocol'] === 'rdp' ? + + : null + } + + { + this.state.attrVisible && this.state.selected['protocol'] === 'vnc' ? + + : null + } + + { + this.state.attrVisible && this.state.selected['protocol'] === 'telnet' ? + + : null + } ); diff --git a/web/src/components/asset/AssetRDPAttributeModal.js b/web/src/components/asset/AssetRDPAttributeModal.js new file mode 100644 index 0000000..5d59b28 --- /dev/null +++ b/web/src/components/asset/AssetRDPAttributeModal.js @@ -0,0 +1,205 @@ +import React from 'react'; +import {Collapse, Form, Input, Modal, Switch, Tooltip, Typography} from "antd/lib/index"; +import {ExclamationCircleOutlined} from "@ant-design/icons"; + +const {Text} = Typography; +const {Panel} = Collapse; +const formLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14}, +}; +const formItemLayout = { + labelCol: {span: 12}, + wrapperCol: {span: 12}, +}; + +const AssetRDPAttributeModal = function ({handleOk, handleCancel, confirmLoading, attributes}) { + + const [form] = Form.useForm(); + + return ( + + { + form + .validateFields() + .then(values => { + form.resetFields(); + handleOk(values); + }) + .catch(info => { + }); + }} + onCancel={handleCancel} + confirmLoading={confirmLoading} + okText='确定' + cancelText='取消' + > + +
+ + + Remote App} key="remote-app"> + + 程序  + } + > + + + + 工作目录  + } + > + + + + 参数  + } + > + + + + 性能} key="2"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ) +} + +export default AssetRDPAttributeModal; diff --git a/web/src/components/asset/AssetSSHAttributeModal.js b/web/src/components/asset/AssetSSHAttributeModal.js new file mode 100644 index 0000000..1b3df49 --- /dev/null +++ b/web/src/components/asset/AssetSSHAttributeModal.js @@ -0,0 +1,107 @@ +import React from 'react'; +import {Collapse, Form, Input, Modal, Select, Typography} from "antd/lib/index"; + +const {Option} = Select; +const {Panel} = Collapse; +const {Text} = Typography; + +const AssetSSHAttributeModal = function ({handleOk, handleCancel, confirmLoading, attributes}) { + + const [form] = Form.useForm(); + + const formItemLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14}, + }; + + return ( + + { + form + .validateFields() + .then(values => { + form.resetFields(); + handleOk(values); + }) + .catch(info => { + }); + }} + onCancel={handleCancel} + confirmLoading={confirmLoading} + okText='确定' + cancelText='取消' + > + +
+ + 显示设置} key="显示设置"> + + + + + + + + + + + + + 控制终端行为} key="控制终端行为"> + + + + + + + + + + +
+
+ ) +} + +export default AssetSSHAttributeModal; diff --git a/web/src/components/asset/AssetTelnetAttributeModal.js b/web/src/components/asset/AssetTelnetAttributeModal.js new file mode 100644 index 0000000..cfea57e --- /dev/null +++ b/web/src/components/asset/AssetTelnetAttributeModal.js @@ -0,0 +1,133 @@ +import React from 'react'; +import {Collapse, Form, Input, Modal, Select, Typography} from "antd/lib/index"; + +const {Option} = Select; +const {Panel} = Collapse; +const {Text} = Typography; + +const AssetTelnetAttributeModal = function ({handleOk, handleCancel, confirmLoading, attributes}) { + + const [form] = Form.useForm(); + + const formItemLayout = { + labelCol: {span: 8}, + wrapperCol: {span: 14}, + }; + + return ( + + { + form + .validateFields() + .then(values => { + form.resetFields(); + handleOk(values); + }) + .catch(info => { + }); + }} + onCancel={handleCancel} + confirmLoading={confirmLoading} + okText='确定' + cancelText='取消' + > + +
+ + 认证} key="认证"> + + + + + + + + + + + + + + 显示设置} key="显示设置"> + + + + + + + + + + + + + 控制终端行为} key="控制终端行为"> + + + + + + + + + + +
+
+ ) +} + +export default AssetTelnetAttributeModal; diff --git a/web/src/components/asset/AssetVNCAttributeModal.js b/web/src/components/asset/AssetVNCAttributeModal.js new file mode 100644 index 0000000..19d10f3 --- /dev/null +++ b/web/src/components/asset/AssetVNCAttributeModal.js @@ -0,0 +1,97 @@ +import React from 'react'; +import {Collapse, Form, Input, Modal, Select, Switch, Tooltip, Typography} from "antd/lib/index"; +import {ExclamationCircleOutlined} from "@ant-design/icons"; + +const {Text} = Typography; + +const {Option} = Select; +const {Panel} = Collapse; +const formLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14}, +}; + +const AssetRDPAttributeModal = function ({handleOk, handleCancel, confirmLoading, attributes}) { + + const [form] = Form.useForm(); + + return ( + + { + form + .validateFields() + .then(values => { + form.resetFields(); + handleOk(values); + }) + .catch(info => { + }); + }} + onCancel={handleCancel} + confirmLoading={confirmLoading} + okText='确定' + cancelText='取消' + > + +
+ + + 显示设置} key="显示设置"> + + + + + + + + + + + + + VNC中继} key="2"> + 目标主机  + } + name='dest-host'> + + + 目标端口  + } + name='dest-port'> + + + + +
+
+ ) +} + +export default AssetRDPAttributeModal; diff --git a/web/src/components/setting/Setting.js b/web/src/components/setting/Setting.js index d962536..648691b 100644 --- a/web/src/components/setting/Setting.js +++ b/web/src/components/setting/Setting.js @@ -312,6 +312,26 @@ class Setting extends Component { SSH配置 + + + + - - - -