- 修复RDP协议连接导致的任意文件读取漏洞

- RDP协议增加「域」参数
- 增加安全访问功能
- 优化代码
This commit is contained in:
dushixiang
2021-03-11 21:16:29 +08:00
parent ba5bff1b38
commit 7f7edaa33c
45 changed files with 1020 additions and 158 deletions

View File

@ -24,6 +24,7 @@ import {
IdcardOutlined,
LinkOutlined,
LoginOutlined,
SafetyCertificateOutlined,
SettingOutlined,
SolutionOutlined,
TeamOutlined,
@ -38,11 +39,12 @@ import BatchCommand from "./components/command/BatchCommand";
import {isEmpty, NT_PACKAGE} from "./utils/utils";
import {isAdmin} from "./service/permission";
import UserGroup from "./components/user/UserGroup";
import LoginLog from "./components/session/LoginLog";
import LoginLog from "./components/devops/LoginLog";
import Term from "./components/access/Term";
import Job from "./components/job/Job";
import Job from "./components/devops/Job";
import {Header} from "antd/es/layout/layout";
import LayoutHeader from "./components/user/LayoutHeader";
import Security from "./components/devops/Security";
const {Footer, Sider} = Layout;
@ -203,6 +205,12 @@ class App extends Component {
计划任务
</Link>
</Menu.Item>
<Menu.Item key="access-security" icon={<SafetyCertificateOutlined/>}>
<Link to={'/access-security'}>
访问安全
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
@ -260,6 +268,7 @@ class App extends Component {
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Route path="/job" component={Job}/>
<Route path="/access-security" component={Security}/>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}

View File

@ -40,7 +40,7 @@ import {
UploadOutlined
} from '@ant-design/icons';
import {PROTOCOL_COLORS} from "../../common/constants";
import LayoutHeader from "../user/LayoutHeader";
import {hasPermission, isAdmin} from "../../service/permission";
import Upload from "antd/es/upload";
import axios from "axios";
@ -880,28 +880,44 @@ class Asset extends Component {
disabled: this.state.fileList.length === 0
}}
>
<Upload
maxCount={1}
onRemove={file => {
this.setState(state => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
return {
fileList: newFileList,
};
});
}}
beforeUpload={(file) => {
this.setState(state => ({
fileList: [file],
}));
return false;
}}
fileList={this.state.fileList}
>
<Button icon={<UploadOutlined/>}>选择csv文件</Button>
</Upload>
<Space>
<Upload
maxCount={1}
onRemove={file => {
this.setState(state => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
return {
fileList: newFileList,
};
});
}}
beforeUpload={(file) => {
this.setState(state => ({
fileList: [file],
}));
return false;
}}
fileList={this.state.fileList}
>
<Button icon={<UploadOutlined/>}>选择csv文件</Button>
</Upload>
<Button type="primary" onClick={() => {
let csvString= 'name,ssh,127.0.0.1,22,username,password,privateKey,passphrase,description';
//前置的"\uFEFF"为“零宽不换行空格”,可处理中文乱码问题
const blob = new Blob(["\uFEFF" + csvString], {type: 'text/csv;charset=gb2312;'});
let a = document.createElement('a');
a.download = 'sample.csv';
a.href = URL.createObjectURL(blob);
a.click();
}}>
下载样本文件
</Button>
</Space>
</Modal>
: undefined
}

View File

@ -287,6 +287,14 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
{
protocol === 'rdp' ?
<>
<Panel header={<Text strong>认证</Text>} key="">
<Form.Item
name="domain"
label='域'
>
<Input type='text' placeholder="身份验证时使用的域"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>Remote App</Text>} key="remote-app">
<Form.Item
name="remote-app"

View File

@ -2,7 +2,7 @@ import React, {Component} from 'react';
import {Card, Input, List, PageHeader, Spin} from "antd";
import Console from "../access/Console";
import {itemRender} from "../../utils/utils";
import LayoutHeader from "../user/LayoutHeader";
import './Command.css'
import request from "../../common/request";
import {message} from "antd/es";

View File

@ -34,7 +34,7 @@ import {
UndoOutlined
} from '@ant-design/icons';
import {compare, itemRender} from "../../utils/utils";
import LayoutHeader from "../user/LayoutHeader";
import {hasPermission, isAdmin} from "../../service/permission";
import dayjs from "dayjs";

View File

@ -33,7 +33,7 @@ import {
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import LayoutHeader from "../user/LayoutHeader";
import {hasPermission, isAdmin} from "../../service/permission";
import dayjs from "dayjs";

View File

@ -6,7 +6,7 @@ import request from "../../common/request";
import './Dashboard.css'
import {Link} from "react-router-dom";
import {Area} from '@ant-design/charts';
import LayoutHeader from "../user/LayoutHeader";
import {isAdmin} from "../../service/permission";
@ -87,9 +87,7 @@ class Dashboard extends Component {
itemRender: itemRender
}}
subTitle="仪表盘"
extra={[
]}
extra={[]}
>
</PageHeader>

View File

@ -31,7 +31,7 @@ import {
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import LayoutHeader from "../user/LayoutHeader";
import dayjs from "dayjs";
import JobModal from "./JobModal";
import './Job.css'

View File

@ -21,7 +21,7 @@ import request from "../../common/request";
import {formatDate, isEmpty, itemRender} from "../../utils/utils";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
import LayoutHeader from "../user/LayoutHeader";
const confirm = Modal.confirm;
const {Content} = Layout;

View File

@ -0,0 +1,421 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import './Job.css'
import SecurityModal from "./SecurityModal";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'security',
breadcrumbName: '访问安全',
}
];
class Security extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/securities/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/securities/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此任务吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, obj = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/securities/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/securities', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/securities/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: 'IP',
dataIndex: 'ip',
key: 'ip',
sorter: true,
}, {
title: '规则',
dataIndex: 'rule',
key: 'rule',
render: (rule) => {
if (rule === 'allow') {
return <Tag color={'green'}>允许</Tag>
} else {
return <Tag color={'red'}>禁止</Tag>
}
}
}, {
title: '优先级',
dataIndex: 'priority',
key: 'priority',
sorter: true,
}, {
title: '来源',
dataIndex: 'source',
key: 'source',
}, {
title: '操作',
key: 'action',
render: (text, record, index) => {
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.showModal('更新计划任务', record)}>编辑</Button>
<Button type="text" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper"
title="访问安全"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="IP访问规则限制"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>IP访问规则列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="IP"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增安全访问规则', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<SecurityModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</SecurityModal> : undefined
}
</Content>
</>
);
}
}
export default Security;

View File

@ -0,0 +1,66 @@
import React from 'react';
import {Form, Input, InputNumber, Modal, Radio} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model['priority'] === undefined) {
model['priority'] = 0;
}
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
centered={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="IP地址" name='ip' rules={[{required: true, message: '请输入IP地址'}]}>
<Input autoComplete="off" placeholder="支持单个IPCIDR或使用-连接的两个IP"/>
</Form.Item>
<Form.Item label="规则" name='rule' rules={[{required: true, message: '请选择规则'}]}>
<Radio.Group onChange={async (e) => {
}}>
<Radio value={'allow'}>允许</Radio>
<Radio value={'reject'}>拒绝</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]}>
<InputNumber min={0} max={100}/>
</Form.Item>
</Form>
</Modal>
)
};
export default SecurityModal;

View File

@ -23,7 +23,7 @@ import Playback from "./Playback";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
import {PROTOCOL_COLORS} from "../../common/constants";
import LayoutHeader from "../user/LayoutHeader";
import dayjs from "dayjs";
const confirm = Modal.confirm;
@ -290,6 +290,30 @@ class OfflineSession extends Component {
<Button type="link" size='small'
disabled={disabled}
onClick={() => this.showPlayback(record)}>回放</Button>
<Button type="link" size='small'
onClick={() => {
confirm({
title: '您确定要禁止该IP访问本系统吗?',
content: '',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
// 向后台提交数据
let formData = {
ip: record['clientIp'],
rule: 'reject',
priority: 99,
}
const result = await request.post('/securities', formData);
if (result.code === 1) {
message.success('禁用成功');
} else {
message.error('禁用失败 :( ' + result.message, 10);
}
}
});
}}>禁用IP</Button>
<Button type="link" size='small' onClick={() => {
confirm({
title: '您确定要删除此会话吗?',
@ -496,7 +520,7 @@ class OfflineSession extends Component {
overflow: 'visible'
}}
onLoad={() => {
// const obj = ReactDOM.findDOMNode(this);
// constant obj = ReactDOM.findDOMNode(this);
// this.setState({
// "iFrameHeight": obj.contentWindow.document.body.scrollHeight + 'px'
// });

View File

@ -24,7 +24,7 @@ import {message} from "antd/es";
import {PROTOCOL_COLORS} from "../../common/constants";
import {DisconnectOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
import Monitor from "../access/Monitor";
import LayoutHeader from "../user/LayoutHeader";
import dayjs from "dayjs";
const confirm = Modal.confirm;

View File

@ -3,7 +3,6 @@ import {Button, Card, Form, Image, Input, Layout, Modal, PageHeader, Result, Spa
import {itemRender} from '../../utils/utils'
import request from "../../common/request";
import {message} from "antd/es";
import LayoutHeader from "./LayoutHeader";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
const {Content} = Layout;

View File

@ -549,7 +549,7 @@ class User extends Component {
{/* icon={<IssuesCloseOutlined/>}*/}
{/* loading={this.state.delBtnLoading}*/}
{/* onClick={() => {*/}
{/* const content = <div>*/}
{/* constant content = <div>*/}
{/* 您确定要启用选中的<Text style={{color: '#1890FF'}}*/}
{/* strong>{this.state.selectedRowKeys.length}</Text>条记录吗?*/}
{/* </div>;*/}
@ -572,7 +572,7 @@ class User extends Component {
{/* <Button type="default" danger disabled={!hasSelected} icon={<StopOutlined/>}*/}
{/* loading={this.state.delBtnLoading}*/}
{/* onClick={() => {*/}
{/* const content = <div>*/}
{/* constant content = <div>*/}
{/* 您确定要禁用选中的<Text style={{color: '#1890FF'}}*/}
{/* strong>{this.state.selectedRowKeys.length}</Text>条记录吗?*/}
{/* </div>;*/}

View File

@ -6,7 +6,6 @@ import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import LayoutHeader from "./LayoutHeader";
import UserGroupModal from "./UserGroupModal";
import UserShareAsset from "./UserShareAsset";
import dayjs from "dayjs";