完善私钥验证

This commit is contained in:
dushixiang
2020-12-25 03:43:15 +08:00
parent 72f7dd5dc6
commit 6c7cb6b0e7
13 changed files with 203 additions and 70 deletions

View File

@ -1,9 +1,9 @@
package api package api
import ( import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model" "next-terminal/pkg/model"
"next-terminal/pkg/utils" "next-terminal/pkg/utils"
"github.com/labstack/echo/v4"
"strconv" "strconv"
"strings" "strings"
) )
@ -51,6 +51,21 @@ func AssetUpdateEndpoint(c echo.Context) error {
if err := c.Bind(&item); err != nil { if err := c.Bind(&item); err != nil {
return err return err
} }
switch item.AccountType {
case "credential":
item.Username = "-"
item.Password = "-"
item.PrivateKey = "-"
item.Passphrase = "-"
case "private-key":
item.Username = "-"
item.Password = "-"
item.CredentialId = "-"
case "custom":
item.PrivateKey = "-"
item.Passphrase = "-"
item.CredentialId = "-"
}
model.UpdateAssetById(&item, id) model.UpdateAssetById(&item, id)

View File

@ -129,6 +129,8 @@ func SessionCreateEndpoint(c echo.Context) error {
AssetId: asset.ID, AssetId: asset.ID,
Username: asset.Username, Username: asset.Username,
Password: asset.Password, Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol, Protocol: asset.Protocol,
IP: asset.IP, IP: asset.IP,
Port: asset.Port, Port: asset.Port,

View File

@ -2,15 +2,14 @@ package api
import ( import (
"bytes" "bytes"
"next-terminal/pkg/model"
"fmt" "fmt"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"log" "log"
"net"
"net/http" "net/http"
"next-terminal/pkg/model"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -57,32 +56,7 @@ func SSHEndpoint(c echo.Context) error {
width, _ := strconv.Atoi(c.QueryParam("width")) width, _ := strconv.Atoi(c.QueryParam("width"))
height, _ := strconv.Atoi(c.QueryParam("height")) height, _ := strconv.Atoi(c.QueryParam("height"))
asset, err := model.FindAssetById(assetId) sshClient, err := CreateSshClient(assetId)
if err != nil {
return err
}
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return err
}
asset.Username = credential.Username
asset.Password = credential.Password
}
config := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: asset.Username,
Auth: []ssh.AuthMethod{ssh.Password(asset.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", asset.IP, asset.Port)
sshClient, err := ssh.Dial("tcp", addr, config)
if err != nil { if err != nil {
return err return err
} }
@ -143,6 +117,55 @@ func SSHEndpoint(c echo.Context) error {
return err return err
} }
func CreateSshClient(assetId string) (*ssh.Client, error) {
asset, err := model.FindAssetById(assetId)
if err != nil {
return nil, err
}
var authMethod ssh.AuthMethod
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return nil, err
}
asset.Username = credential.Username
asset.Password = credential.Password
authMethod = ssh.Password(asset.Password)
} else if asset.AccountType == "private-key" {
var key ssh.Signer
if len(asset.Passphrase) > 0 {
key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(asset.PrivateKey), []byte(asset.Passphrase))
if err != nil {
return nil, err
}
} else {
key, err = ssh.ParsePrivateKey([]byte(asset.PrivateKey))
if err != nil {
return nil, err
}
}
authMethod = ssh.PublicKeys(key)
} else {
authMethod = ssh.Password(asset.Password)
}
config := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: asset.Username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
addr := fmt.Sprintf("%s:%d", asset.IP, asset.Port)
sshClient, err := ssh.Dial("tcp", addr, config)
if err != nil {
return nil, err
}
return sshClient, nil
}
func WriteMessage(ws *websocket.Conn, message string) { func WriteMessage(ws *websocket.Conn, message string) {
WriteByteMessage(ws, []byte(message)) WriteByteMessage(ws, []byte(message))
} }
@ -154,19 +177,8 @@ func WriteByteMessage(ws *websocket.Conn, p []byte) {
} }
} }
func CreateSftpClient(username, password, ip string, port int) (sftpClient *sftp.Client, err error) { func CreateSftpClient(assetId string) (sftpClient *sftp.Client, err error) {
clientConfig := &ssh.ClientConfig{ sshClient, err := CreateSshClient(assetId)
Timeout: 1 * time.Second,
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", ip, port)
sshClient, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -80,10 +80,15 @@ func TunEndpoint(c echo.Context) error {
configuration.SetParameter("enable-sftp", "") configuration.SetParameter("enable-sftp", "")
break break
case "ssh": case "ssh":
if session.PrivateKey == "-" {
configuration.SetParameter("username", session.Username) configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password) configuration.SetParameter("password", session.Password)
} else {
configuration.SetParameter("private-key", session.PrivateKey)
configuration.SetParameter("passphrase", session.Passphrase)
}
sftpClient, err = CreateSftpClient(session.Username, session.Password, session.IP, session.Port) sftpClient, err = CreateSftpClient(session.AssetId)
if err != nil { if err != nil {
return err return err
} }

View File

@ -27,6 +27,8 @@ type Session struct {
Height int `json:"height"` Height int `json:"height"`
Status string `json:"status"` Status string `json:"status"`
Recording string `json:"recording"` Recording string `json:"recording"`
PrivateKey string `json:"privateKey"`
Passphrase string `json:"passphrase"`
ConnectedTime utils.JsonTime `json:"connectedTime"` ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"` DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
} }

69
web/public/bg.svg Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -14,7 +14,7 @@
--> -->
<title>Next Terminal</title> <title>Next Terminal</title>
</head> </head>
<body style="background-color: darkgrey"> <body style="background-color: #8c8c8c">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,13 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {Button, Card, Checkbox, Form, Input} from "antd"; import {Button, Card, Checkbox, Form, Input, Typography} from "antd";
import './Login.css' import './Login.css'
import request from "../common/request"; import request from "../common/request";
import {message} from "antd/es"; import {message} from "antd/es";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import { import {LockOutlined, UserOutlined} from '@ant-design/icons';
UserOutlined, LockOutlined
} from '@ant-design/icons';
const {Title} = Typography;
class LoginForm extends Component { class LoginForm extends Component {
@ -48,9 +49,12 @@ class LoginForm extends Component {
}; };
render() { render() {
return ( return (
<Card className='login-card' title="登录"> <div className='login-bg' style={{width: window.innerWidth, height: window.innerHeight, backgroundColor: '#F0F2F5'}}>
<Card className='login-card' title={null}>
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
<Title level={1}>Next Terminal</Title>
</div>
<Form onFinish={this.handleSubmit} className="login-form"> <Form onFinish={this.handleSubmit} className="login-form">
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}> <Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
<Input prefix={<UserOutlined/>} placeholder="登录账号"/> <Input prefix={<UserOutlined/>} placeholder="登录账号"/>
@ -71,6 +75,8 @@ class LoginForm extends Component {
</Form.Item> </Form.Item>
</Form> </Form>
</Card> </Card>
</div>
); );
} }
} }

View File

@ -766,12 +766,12 @@ class Access extends Component {
return ( return (
<div> <div>
<div className="container" style={{ <div className="container" style={{
overflow: this.state.containerOverflow, overflow: this.state.containerOverflow,
width: this.state.containerWidth, width: this.state.containerWidth,
height: this.state.containerHeight height: this.state.containerHeight
}}> }}>
<div id="display"/> <div id="display"/>
</div> </div>

View File

@ -17,6 +17,14 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
setAccountType(model.accountType); setAccountType(model.accountType);
}); });
for (let key in model) {
if (model.hasOwnProperty(key)) {
if (model[key] === '-') {
model[key] = '';
}
}
}
const formItemLayout = { const formItemLayout = {
labelCol: {span: 6}, labelCol: {span: 6},
wrapperCol: {span: 14}, wrapperCol: {span: 14},
@ -54,7 +62,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
<Modal <Modal
title={title} title={title}
visible={visible} visible={visible}
maskClosable={true} maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
@ -62,7 +70,8 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
form.resetFields(); form.resetFields();
handleOk(values); handleOk(values);
}) })
.catch(info => {}); .catch(info => {
});
}} }}
onCancel={handleCancel} onCancel={handleCancel}
confirmLoading={confirmLoading} confirmLoading={confirmLoading}
@ -103,7 +112,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
}}> }}>
<Option value="custom">自定义</Option> <Option value="custom">自定义</Option>
<Option value="credential">授权凭证</Option> <Option value="credential">授权凭证</Option>
<Option value="secret-key"></Option> <Option value="private-key"></Option>
</Select> </Select>
</Form.Item> </Form.Item>
@ -143,10 +152,17 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
} }
{ {
accountType === 'secret-key' ? accountType === 'private-key' ?
<Form.Item label="私钥" name='passphrase' rules={[{required: true, message: '请输入私钥'}]}> <>
<Form.Item label="私钥" name='privateKey' rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/> <TextArea rows={4}/>
</Form.Item> </Form.Item>
<Form.Item label="私钥密码" name='passphrase'>
<TextArea rows={1}/>
</Form.Item>
</>
: null : null
} }
</Form> </Form>

View File

@ -481,12 +481,14 @@ class OfflineSession extends Component {
<Modal <Modal
title="会话回放" title="会话回放"
centered
visible={this.state.playbackVisible} visible={this.state.playbackVisible}
onCancel={this.hidePlayback} onCancel={this.hidePlayback}
width={window.innerWidth * 0.8} width={window.innerWidth * 0.8}
footer={null} footer={null}
destroyOnClose destroyOnClose
maskClosable={false}
> >
<Playback sessionId={this.state.playbackSessionId}/> <Playback sessionId={this.state.playbackSessionId}/>
</Modal> </Modal>

View File

@ -477,6 +477,7 @@ class OnlineSession extends Component {
className='monitor' className='monitor'
title={this.state.sessionTitle} title={this.state.sessionTitle}
centered centered
maskClosable={false}
visible={this.state.accessVisible} visible={this.state.accessVisible}
footer={null} footer={null}
width={window.innerWidth * 0.8} width={window.innerWidth * 0.8}