Skip to content

Commit

Permalink
feat: ssh 直接登录资产,同时支持私钥认证 (#434)
Browse files Browse the repository at this point in the history
  • Loading branch information
VaalaCat authored Jan 13, 2024
1 parent 407eb81 commit e38a262
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 18 deletions.
3 changes: 2 additions & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ guacd:
sshd:
enable: true
addr: 0.0.0.0:8089
key: ~/.ssh/id_rsa
key: ~/.ssh/id_rsa
authorized-keys: ~/.ssh/authorized_keys
3 changes: 2 additions & 1 deletion config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ guacd:
sshd:
enable: true
addr: 0.0.0.0:2022
key: ~/.ssh/id_rsa
key: ~/.ssh/id_rsa
authorized-keys: ~/.ssh/authorized_keys
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.4
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cast v1.5.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
Expand Down Expand Up @@ -58,7 +59,6 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
Expand Down
15 changes: 9 additions & 6 deletions server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ type Guacd struct {
}

type Sshd struct {
Enable bool
Addr string
Key string
Enable bool
Addr string
Key string
AuthorizedKeys string
}

func SetupConfig() (*Config, error) {
Expand Down Expand Up @@ -103,6 +104,7 @@ func SetupConfig() (*Config, error) {
pflag.Bool("sshd.enable", false, "true or false")
pflag.String("sshd.addr", "", "sshd server listen addr")
pflag.String("sshd.key", "~/.ssh/id_rsa", "sshd public key filepath")
pflag.String("sshd.authorized-keys", "/root/.ssh/authorized_keys", "sshd authorized keys filepath")

pflag.Parse()
if err := viper.BindPFlags(pflag.CommandLine); err != nil {
Expand Down Expand Up @@ -156,9 +158,10 @@ func SetupConfig() (*Config, error) {
Drive: guacdDrive,
},
Sshd: &Sshd{
Enable: viper.GetBool("sshd.enable"),
Addr: viper.GetString("sshd.addr"),
Key: sshdKey,
Enable: viper.GetBool("sshd.enable"),
Addr: viper.GetString("sshd.addr"),
Key: sshdKey,
AuthorizedKeys: viper.GetString("sshd.authorized-keys"),
},
}

Expand Down
5 changes: 5 additions & 0 deletions server/repository/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ func (r assetRepository) FindAttrById(c context.Context, assetId string) (o []mo
return o, err
}

func (r assetRepository) FindAssetByName(c context.Context, name string, protocol string) (o model.Asset, err error) {
err = r.GetDB(c).Where("name = ? and protocol = ?", name, protocol).First(&o).Error
return
}

func (r assetRepository) FindAssetAttrMapByAssetId(c context.Context, assetId string) (map[string]string, error) {
asset, err := r.FindById(c, assetId)
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions server/service/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package service

import (
"context"
"fmt"
"next-terminal/server/common/sets"
"next-terminal/server/model"
"next-terminal/server/repository"
Expand All @@ -27,6 +28,26 @@ func (s *workerService) FindMyAssetPaging(pageIndex, pageSize int, name, protoco
return items, total, nil
}

func (s *workerService) FindMyAssetByName(name, protocol, userId string) (o model.Asset, err error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
return model.Asset{}, err
}
item, err := repository.AssetRepository.FindAssetByName(context.Background(), name, protocol)
if err != nil {
return model.Asset{}, err
}

if len(assetIdList) > 0 {
for _, id := range assetIdList {
if item.ID == id {
return item, nil
}
}
}
return model.Asset{}, fmt.Errorf("资产不存在")
}

func (s *workerService) FindMyAsset(name, protocol, tags string, userId string, order, field string) (o []model.AssetForPage, err error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
Expand Down
81 changes: 79 additions & 2 deletions server/sshd/sshd.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package sshd

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"net"
"next-terminal/server/common/nt"
"os"
"strings"

"next-terminal/server/branding"
Expand Down Expand Up @@ -107,7 +109,12 @@ func (sshd sshd) sessionHandler(sess ssh.Session) {
_ = sess.Close()
}()

username := sess.User()
rawReq := strings.Split(sess.User(), "@")
username := rawReq[0]
if len(rawReq) == 2 {
assetname := rawReq[1]
sess.Context().SetValue("publicKeyAsset", assetname)
}
remoteAddr := strings.Split(sess.RemoteAddr().String(), ":")[0]

user, err := repository.UserRepository.FindByUsername(context.TODO(), username)
Expand All @@ -120,16 +127,85 @@ func (sshd sshd) sessionHandler(sess ssh.Session) {
return
}

if sess.PublicKey() != nil {
publicKeyComment, ok := sess.Context().Value("publicKeyComment").(string)
if publicKeyComment == "" && !ok {
return
}

if user.Username != publicKeyComment {
_, _ = io.WriteString(sess, "您输入的账户或密码不正确.\n")
return
}
}

// 判断是否需要进行双因素认证
if user.TOTPSecret != "" && user.TOTPSecret != "-" {
if user.TOTPSecret != "" && user.TOTPSecret != "-" && sess.PublicKey() == nil {
sshd.gui.totpUI(sess, user, remoteAddr, username)
} else {
// 保存登录日志
_ = service.UserService.SaveLoginLog(remoteAddr, "terminal", username, true, false, utils.LongUUID(), "")
if sess.PublicKey() != nil {
_, _ = io.WriteString(sess, "\n公钥认证成功\n")
}
sshd.gui.MainUI(sess, user)
}
}

func (sshd sshd) publicKeyAuth(ctx ssh.Context, key ssh.PublicKey) bool {
if len(config.GlobalCfg.Sshd.AuthorizedKeys) == 0 {
return false
}
f, err := os.Open(config.GlobalCfg.Sshd.AuthorizedKeys)
if err != nil {
fmt.Printf("failed to open authorized_keys file: %v\n", err)
return false
}
defer f.Close()

keys := []struct {
key ssh.PublicKey
comment string
}{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
t := scanner.Text()
if t == "" {
continue
}
if strings.HasPrefix(t, "#") {
continue
}
pk, c, _, _, err := ssh.ParseAuthorizedKey([]byte(t))
if err != nil {
continue
}
keys = append(keys, struct {
key ssh.PublicKey
comment string
}{
key: pk,
comment: c,
})
}

fmt.Printf("public key: %+v\n", keys)

if err := scanner.Err(); err != nil {
return false
}

// check if the public key is in the authorized_keys file
for _, k := range keys {
if ssh.KeysEqual(key, k.key) {
ctx.SetValue("publicKeyComment", k.comment)
return true
}
}

return false
}

func (sshd sshd) Serve() {
ssh.Handle(func(s ssh.Session) {
_, _ = io.WriteString(s, branding.Hi)
Expand All @@ -140,6 +216,7 @@ func (sshd sshd) Serve() {
err := ssh.ListenAndServe(
config.GlobalCfg.Sshd.Addr,
nil,
ssh.PublicKeyAuth(sshd.publicKeyAuth),
ssh.PasswordAuth(sshd.passwordAuth),
ssh.HostKeyFile(config.GlobalCfg.Sshd.Key),
ssh.WrapConn(sshd.connCallback),
Expand Down
45 changes: 38 additions & 7 deletions server/sshd/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,17 @@ func (gui Gui) MainUI(sess ssh.Session, user model.User) {

MainLoop:
for {
_, result, err := prompt.Run()
var (
result string
err error
)
publicKeyAsset, ok := sess.Context().Value("publicKeyAsset").(string)
if ok && publicKeyAsset != "" {
gui.AssetUI(sess, user)
return
} else {
_, result, err = prompt.Run()
}
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
Expand All @@ -56,6 +66,19 @@ MainLoop:
}

func (gui Gui) AssetUI(sess ssh.Session, user model.User) {
var (
selectedAssetId string
err error
)
publicKeyAsset, ok := sess.Context().Value("publicKeyAsset").(string)
if ok && publicKeyAsset != "" {
asset, err := service.WorkerService.FindMyAssetByName(publicKeyAsset, nt.SSH, user.ID)
if err != nil || asset.ID == "" {
sess.Write([]byte("未找到对应资产\n"))
return
}
selectedAssetId = asset.ID
}
assetsNoSort, err := service.WorkerService.FindMyAsset("", nt.SSH, "", user.ID, "", "")
if err != nil {
return
Expand Down Expand Up @@ -105,14 +128,22 @@ func (gui Gui) AssetUI(sess ssh.Session, user model.User) {

AssetUILoop:
for {
i, _, err := prompt.Run()

if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
var (
err error
i int
chooseAssetId string
)
if len(selectedAssetId) != 0 {
chooseAssetId = selectedAssetId
} else {
i, _, err = prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
chooseAssetId = assets[i].ID
}

chooseAssetId := assets[i].ID
switch chooseAssetId {
case "quit":
break AssetUILoop
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^4.7.0",
"@ant-design/plots": "^2.1.11",
"@ant-design/pro-components": "1.1.21",
"@turf/bbox": "^6.5.0",
"antd": "4.23.5",
Expand Down

0 comments on commit e38a262

Please sign in to comment.