2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 01:57:19 +08:00

feat: ssh支持私钥密码,close #774

This commit is contained in:
2026-01-08 23:24:04 +08:00
parent 3afc00be2f
commit 89ace2360c
5 changed files with 61 additions and 14 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/leonelquinteros/gotext"
cryptossh "golang.org/x/crypto/ssh"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/biz"
@@ -47,11 +48,13 @@ func (r *sshRepo) Create(req *request.SSHCreate) error {
User: req.User,
Password: req.Password,
Key: req.Key,
Passphrase: req.Passphrase,
}
_, err := pkgssh.NewSSHClient(conf)
client, err := pkgssh.NewSSHClient(conf)
if err != nil {
return errors.New(r.t.Get("failed to check ssh connection: %v", err))
}
defer func(client *cryptossh.Client) { _ = client.Close() }(client)
ssh := &biz.SSH{
Name: req.Name,
@@ -71,11 +74,13 @@ func (r *sshRepo) Update(req *request.SSHUpdate) error {
User: req.User,
Password: req.Password,
Key: req.Key,
Passphrase: req.Passphrase,
}
_, err := pkgssh.NewSSHClient(conf)
client, err := pkgssh.NewSSHClient(conf)
if err != nil {
return errors.New(r.t.Get("failed to check ssh connection: %v", err))
}
defer func(client *cryptossh.Client) { _ = client.Close() }(client)
ssh := &biz.SSH{
ID: req.ID,

View File

@@ -5,9 +5,10 @@ type SSHCreate struct {
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required|in:password,publickey"`
User string `json:"user" form:"user" validate:"requiredIf:AuthMethod,password"`
User string `json:"user" form:"user" validate:"required"`
Password string `json:"password" form:"password" validate:"requiredIf:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"requiredIf:AuthMethod,publickey"`
Passphrase string `json:"passphrase" form:"passphrase"`
Remark string `json:"remark" form:"remark"`
}
@@ -17,8 +18,9 @@ type SSHUpdate struct {
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required|in:password,publickey"`
User string `json:"user" form:"user" validate:"requiredIf:AuthMethod,password"`
User string `json:"user" form:"user" validate:"required"`
Password string `json:"password" form:"password" validate:"requiredIf:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"requiredIf:AuthMethod,publickey"`
Passphrase string `json:"passphrase" form:"passphrase"`
Remark string `json:"remark" form:"remark"`
}

View File

@@ -1,6 +1,7 @@
package ssh
import (
"errors"
"time"
"golang.org/x/crypto/ssh"
@@ -19,26 +20,28 @@ type ClientConfig struct {
User string `json:"user"`
Password string `json:"password"`
Key string `json:"key"`
Passphrase string `json:"passphrase"`
Timeout time.Duration `json:"timeout"`
}
func ClientConfigPassword(host, user, Password string) *ClientConfig {
func ClientConfigPassword(host, user, password string) *ClientConfig {
return &ClientConfig{
Timeout: 10 * time.Second,
AuthMethod: PASSWORD,
Host: host,
User: user,
Password: Password,
Password: password,
}
}
func ClientConfigPublicKey(host, user, key string) *ClientConfig {
func ClientConfigPublicKey(host, user, key, passphrase string) *ClientConfig {
return &ClientConfig{
Timeout: 10 * time.Second,
AuthMethod: PUBLICKEY,
Host: host,
User: user,
Key: key,
Passphrase: passphrase,
}
}
@@ -57,7 +60,7 @@ func NewSSHClient(conf ClientConfig) (*ssh.Client, error) {
case PASSWORD:
config.Auth = []ssh.AuthMethod{ssh.Password(conf.Password)}
case PUBLICKEY:
signer, err := parseKey(conf.Key)
signer, err := parseKey(conf.Key, conf.Passphrase)
if err != nil {
return nil, err
}
@@ -71,6 +74,23 @@ func NewSSHClient(conf ClientConfig) (*ssh.Client, error) {
return c, nil
}
func parseKey(key string) (ssh.Signer, error) {
return ssh.ParsePrivateKey([]byte(key))
// parseKey 解析私钥
func parseKey(key, passphrase string) (ssh.Signer, error) {
keyBytes := []byte(key)
if passphrase != "" {
return ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(passphrase))
}
signer, err := ssh.ParsePrivateKey(keyBytes)
if err != nil {
// 密钥被加密
var passphraseMissingError *ssh.PassphraseMissingError
if errors.As(err, &passphraseMissingError) {
return nil, passphraseMissingError
}
return nil, err
}
return signer, nil
}

View File

@@ -15,6 +15,7 @@ const model = ref({
user: 'root',
password: '',
key: '',
passphrase: '',
remark: ''
})
@@ -32,6 +33,7 @@ const handleSubmit = () => {
user: 'root',
password: '',
key: '',
passphrase: '',
remark: ''
}
window.$bus.emit('ssh:refresh')
@@ -80,14 +82,22 @@ const handleSubmit = () => {
>
</n-select>
</n-form-item>
<n-form-item v-if="model.auth_method == 'password'" :label="$gettext('Username')">
<n-form-item :label="$gettext('Username')">
<n-input v-model:value="model.user" placeholder="root" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'password'" :label="$gettext('Password')">
<n-input v-model:value="model.password" type="password" show-password-on="click" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'publickey'" :label="$gettext('Private Key')">
<n-input v-model:value="model.key" type="textarea" />
<n-input v-model:value="model.key" type="textarea" :rows="5" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'publickey'" :label="$gettext('Key Passphrase')">
<n-input
v-model:value="model.passphrase"
type="password"
show-password-on="click"
:placeholder="$gettext('Leave empty if key has no passphrase')"
/>
</n-form-item>
<n-form-item :label="$gettext('Remarks')">
<n-input v-model:value="model.remark" type="textarea" />

View File

@@ -16,6 +16,7 @@ const model = ref({
user: 'root',
password: '',
key: '',
passphrase: '',
remark: ''
})
@@ -44,6 +45,7 @@ watch(show, async () => {
model.value.user = data.config.user
model.value.password = data.config.password
model.value.key = data.config.key
model.value.passphrase = data.config.passphrase || ''
model.value.remark = data.remark
}
})
@@ -86,14 +88,22 @@ watch(show, async () => {
>
</n-select>
</n-form-item>
<n-form-item v-if="model.auth_method == 'password'" :label="$gettext('Username')">
<n-form-item :label="$gettext('Username')">
<n-input v-model:value="model.user" placeholder="root" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'password'" :label="$gettext('Password')">
<n-input v-model:value="model.password" type="password" show-password-on="click" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'publickey'" :label="$gettext('Private Key')">
<n-input v-model:value="model.key" type="textarea" />
<n-input v-model:value="model.key" type="textarea" :rows="5" />
</n-form-item>
<n-form-item v-if="model.auth_method == 'publickey'" :label="$gettext('Key Passphrase')">
<n-input
v-model:value="model.passphrase"
type="password"
show-password-on="click"
:placeholder="$gettext('Leave empty if key has no passphrase')"
/>
</n-form-item>
<n-form-item :label="$gettext('Remarks')">
<n-input v-model:value="model.remark" type="textarea" />