mirror of
https://github.com/veops/oneterm.git
synced 2025-11-01 19:32:37 +08:00
feat: add ssh server
This commit is contained in:
539
backend/pkg/proto/ssh/handler/proxy.go
Normal file
539
backend/pkg/proto/ssh/handler/proxy.go
Normal file
@@ -0,0 +1,539 @@
|
||||
// Package handler
|
||||
/**
|
||||
Copyright (c) The Authors.
|
||||
* @Author: feng.xiang
|
||||
* @Date: 2024/1/18 17:05
|
||||
* @Desc:
|
||||
*/
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
gossh "github.com/gliderlabs/ssh"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
myi18n "github.com/veops/oneterm/pkg/i18n"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/proto/ssh/client"
|
||||
"github.com/veops/oneterm/pkg/proto/ssh/config"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
)
|
||||
|
||||
func (i *InteractiveHandler) Proxy(line string, accountId int) (step int, err error) {
|
||||
step = 1
|
||||
if accountId > 0 {
|
||||
var accountName string
|
||||
defer func() {
|
||||
if err != nil {
|
||||
i.PrintMessage(myi18n.MsgSshAccountLoginError, map[string]any{"User": accountName})
|
||||
}
|
||||
}()
|
||||
if i.SelectedAsset != nil && accountId < 0 {
|
||||
accountName = line
|
||||
}
|
||||
host, ok, er := i.Check(i.SelectedAsset.Id, nil)
|
||||
if er != nil {
|
||||
err = er
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
err = fmt.Errorf("current time is not allowed to access")
|
||||
return
|
||||
}
|
||||
i.SelectedAsset = host
|
||||
if _, ok := i.SelectedAsset.Authorization[accountId]; !ok {
|
||||
err = fmt.Errorf("you donnot have permission")
|
||||
return
|
||||
}
|
||||
account, er := i.Sshd.Core.Auth.AccountInfo(i.Session.Context().Value("cookie").(string), accountId, accountName)
|
||||
if er != nil {
|
||||
err = er
|
||||
return
|
||||
}
|
||||
accountName = account.Name
|
||||
var gateway *model.Gateway
|
||||
if i.SelectedAsset.GatewayId != 0 {
|
||||
gateway, err = i.Sshd.Core.Asset.Gateway(i.Session.Context().Value("cookie").(string), i.SelectedAsset.GatewayId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
conn, er := i.NewSession(account, gateway)
|
||||
if er != nil {
|
||||
err = er
|
||||
return
|
||||
}
|
||||
er = i.UpsertSession(conn, model.SESSIONSTATUS_ONLINE)
|
||||
if er != nil {
|
||||
logger.L.Warn(er.Error())
|
||||
}
|
||||
config.TotalHostSession.Store(conn.SessionId, conn)
|
||||
|
||||
if err := i.bind(i.Session, conn); err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
step = 1
|
||||
} else {
|
||||
if i.NeedAccount && i.SelectedAsset != nil {
|
||||
i.NeedAccount = false
|
||||
var account *model.Account
|
||||
accountIds := make([]int, 0)
|
||||
for aId := range i.SelectedAsset.Authorization {
|
||||
accountIds = append(accountIds, aId)
|
||||
}
|
||||
sort.Ints(accountIds)
|
||||
for _, aId := range accountIds {
|
||||
account := i.Accounts[aId]
|
||||
if config.SSHConfig.PlainMode {
|
||||
if account == nil || !strings.Contains(account.Name, line) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if account == nil || line != account.Name {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return i.Proxy(line, account.Id)
|
||||
}
|
||||
if account == nil {
|
||||
i.PrintMessage(myi18n.MsgSshAccountLoginError, map[string]any{"User": line})
|
||||
}
|
||||
return
|
||||
} else if strings.TrimSpace(line) == "" {
|
||||
return
|
||||
}
|
||||
var (
|
||||
host *model.Asset
|
||||
)
|
||||
selectHosts, likeHosts, er := i.AcquireAndStoreAssets(line, 0)
|
||||
if er != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
if len(selectHosts) == 0 && len(likeHosts) == 0 {
|
||||
i.PrintMessage(myi18n.MsgSshNoMatchingAsset, map[string]any{"Host": line})
|
||||
return
|
||||
}
|
||||
|
||||
switch len(selectHosts) {
|
||||
case 0:
|
||||
switch len(likeHosts) {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
host = likeHosts[0]
|
||||
default:
|
||||
i.showResult(likeHosts)
|
||||
return
|
||||
}
|
||||
case 1:
|
||||
host = selectHosts[0]
|
||||
default:
|
||||
i.showResult(selectHosts)
|
||||
return
|
||||
}
|
||||
i.SelectedAsset = host
|
||||
|
||||
var sshPort string
|
||||
for _, v := range host.Protocols {
|
||||
tmp := strings.Split(v, ":")
|
||||
if len(tmp) == 2 && tmp[0] == "ssh" {
|
||||
sshPort = tmp[1]
|
||||
}
|
||||
}
|
||||
if sshPort == "" {
|
||||
i.PrintMessage(myi18n.MsgSshNoSshAccessMethod, map[string]any{"Host": line})
|
||||
return
|
||||
}
|
||||
i.SessionReq.Protocol = "ssh:" + sshPort
|
||||
|
||||
var hostAccountIds []int
|
||||
if len(i.Accounts) == 0 {
|
||||
accounts, er := i.AcquireAccounts()
|
||||
if er != nil {
|
||||
logger.L.Info(er.Error())
|
||||
} else {
|
||||
i.Accounts = accounts
|
||||
}
|
||||
}
|
||||
for k := range host.Authorization {
|
||||
if v, ok := i.Accounts[k]; ok {
|
||||
hostAccountIds = append(hostAccountIds, v.Id)
|
||||
}
|
||||
}
|
||||
sort.Ints(hostAccountIds)
|
||||
switch len(hostAccountIds) {
|
||||
case 0:
|
||||
i.PrintMessage(myi18n.MsgSshNoSshAccountForAsset, map[string]any{"Host": line})
|
||||
return
|
||||
case 1:
|
||||
return i.Proxy(line, hostAccountIds[0])
|
||||
default:
|
||||
var accounts []string
|
||||
for _, aId := range hostAccountIds {
|
||||
if account, ok := i.Accounts[aId]; ok {
|
||||
i.AccountsForSelect = append(i.AccountsForSelect, account)
|
||||
accounts = append(accounts, account.Name)
|
||||
}
|
||||
}
|
||||
i.NeedAccount = true
|
||||
if config.SSHConfig.PlainMode {
|
||||
i.PrintMessage(myi18n.MsgSshMultiSshAccountForAsset, map[string]any{"Accounts": i.tableData(accounts)})
|
||||
}
|
||||
step = 2
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) bind(userConn gossh.Session, hostConn *client.Connection) error {
|
||||
maxIdelTimeout := time.Hour * 2
|
||||
idleTimeout := time.Second * 60 * 5
|
||||
mConfig, _ := i.AcquireConfig()
|
||||
if mConfig != nil && mConfig.Timeout > 0 {
|
||||
idleTimeout = time.Second * time.Duration(mConfig.Timeout)
|
||||
}
|
||||
if idleTimeout > maxIdelTimeout {
|
||||
idleTimeout = maxIdelTimeout
|
||||
}
|
||||
|
||||
targetInChan := make(chan []byte, 1)
|
||||
targetOutChan := make(chan []byte, 1)
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
exit bool
|
||||
accessUpdateStep = time.Minute
|
||||
waitRead atomic.Bool
|
||||
)
|
||||
waitRead.Store(true)
|
||||
i.wrapJsonResponse(hostConn.SessionId, 0, "success")
|
||||
tk, tkAccess, readReset := time.NewTicker(idleTimeout), time.NewTicker(accessUpdateStep), time.NewTicker(time.Second)
|
||||
go func() {
|
||||
buffer := bytes.NewBuffer(make([]byte, 0, 1024*2))
|
||||
maxLen := 1024
|
||||
for {
|
||||
if !waitRead.Load() {
|
||||
if exit {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
continue
|
||||
}
|
||||
|
||||
buf := make([]byte, maxLen)
|
||||
nr, err := userConn.Read(buf)
|
||||
waitRead.Store(config.SSHConfig.PlainMode)
|
||||
if err != nil {
|
||||
logger.L.Info(err.Error())
|
||||
}
|
||||
|
||||
if nr > 0 {
|
||||
validBytes := buf[:nr]
|
||||
bufferLen := buffer.Len()
|
||||
if bufferLen > 0 || nr == maxLen {
|
||||
buffer.Write(buf[:nr])
|
||||
validBytes = validBytes[:0]
|
||||
}
|
||||
remainBytes := buffer.Bytes()
|
||||
for len(remainBytes) > 0 {
|
||||
r, size := utf8.DecodeRune(remainBytes)
|
||||
if r == utf8.RuneError {
|
||||
if len(remainBytes) <= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
validBytes = append(validBytes, remainBytes[:size]...)
|
||||
remainBytes = remainBytes[size:]
|
||||
}
|
||||
buffer.Reset()
|
||||
if len(remainBytes) > 0 {
|
||||
buffer.Write(remainBytes)
|
||||
}
|
||||
select {
|
||||
case targetInChan <- validBytes:
|
||||
case <-done:
|
||||
break
|
||||
}
|
||||
}
|
||||
if exit {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(targetInChan)
|
||||
}()
|
||||
go func() {
|
||||
defer func() {
|
||||
waitRead.Store(config.SSHConfig.PlainMode)
|
||||
}()
|
||||
for {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := hostConn.Stdout.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
close(done)
|
||||
exit = true
|
||||
} else {
|
||||
logger.L.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if exit {
|
||||
waitRead.Store(config.SSHConfig.PlainMode)
|
||||
} else {
|
||||
waitRead.Store(true)
|
||||
}
|
||||
|
||||
select {
|
||||
case targetOutChan <- buf[:n]:
|
||||
case <-done:
|
||||
break
|
||||
}
|
||||
if exit || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
exit = true
|
||||
|
||||
waitRead.Store(config.SSHConfig.PlainMode)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case p, ok := <-targetOutChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
_, err := userConn.Write(p)
|
||||
if err != nil {
|
||||
logger.L.Info(err.Error())
|
||||
}
|
||||
|
||||
err = i.HandleData("output", p, hostConn, targetOutChan)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
tk.Reset(idleTimeout)
|
||||
case p, ok := <-targetInChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
tk.Reset(idleTimeout)
|
||||
//readL.WriteStdin(p)
|
||||
err := i.HandleData("input", p, hostConn, targetOutChan)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
readReset.Reset(time.Second)
|
||||
case <-done:
|
||||
break
|
||||
case <-tk.C:
|
||||
_, err := userConn.Write([]byte(i.Message(myi18n.MsgSShHostIdleTimeout, map[string]any{"Idle": idleTimeout})))
|
||||
|
||||
if err != nil {
|
||||
logger.L.Warn(err.Error())
|
||||
}
|
||||
exit = true
|
||||
case <-readReset.C:
|
||||
waitRead.Store(true)
|
||||
case <-hostConn.Exit:
|
||||
exit = true
|
||||
case <-i.Session.Context().Done():
|
||||
exit = true
|
||||
case <-tkAccess.C:
|
||||
commands, er := i.AcquireCommands()
|
||||
if er == nil {
|
||||
i.Commands = commands
|
||||
}
|
||||
asset, er := i.AcquireAssets("", hostConn.AssetId)
|
||||
if er != nil || len(asset) <= 0 || !i.Sshd.Core.Asset.HasPermission(asset[0].AccessAuth) {
|
||||
_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshAccessRefusedInTimespan, nil)))
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
exit = true
|
||||
break
|
||||
}
|
||||
i.SelectedAsset = asset[0]
|
||||
if _, ok := asset[0].Authorization[hostConn.AccountId]; !ok {
|
||||
_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshNoAssetPermission, map[string]any{"Host": i.SelectedAsset.Name})))
|
||||
if err != nil {
|
||||
logger.L.Warn(err.Error())
|
||||
}
|
||||
exit = true
|
||||
break
|
||||
}
|
||||
account, er := i.AcquireAccountInfo(hostConn.AccountId, "")
|
||||
if er != nil || account == nil {
|
||||
_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshNoAssetPermission, map[string]any{"Host": i.SelectedAsset.Name})))
|
||||
if err != nil {
|
||||
logger.L.Warn(err.Error())
|
||||
}
|
||||
exit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exit {
|
||||
i.Exits(hostConn)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) CommandLevel(cmd string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseOutput(data []string) (output []string) {
|
||||
for _, line := range data {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
output = append(output, line)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) Output() string {
|
||||
i.Parser.Output.Feed(i.Parser.OutputData)
|
||||
|
||||
res := parseOutput(i.Parser.Output.Listener.Display())
|
||||
if len(res) == 0 {
|
||||
return ""
|
||||
}
|
||||
return res[len(res)-1]
|
||||
}
|
||||
func (i *InteractiveHandler) Command() string {
|
||||
s := i.Output()
|
||||
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(s), strings.TrimSpace(i.Parser.Ps1)))
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) HandleData(src string, data []byte, hostConn *client.Connection,
|
||||
targetOutputChan chan<- []byte) (err error) {
|
||||
switch src {
|
||||
case "input": // input from user
|
||||
if hostConn.Parser.State(data) {
|
||||
_, err = hostConn.Stdin.Write(data)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
var write bool
|
||||
if bytes.LastIndex(data, []byte{13}) != 0 {
|
||||
_, err = hostConn.Stdin.Write(data)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
write = true
|
||||
}
|
||||
if len(i.Parser.InputData) == 0 && i.Parser.Ps2 == "" {
|
||||
i.Parser.Ps1 = i.Output()
|
||||
}
|
||||
i.Parser.InputData = append(i.Parser.InputData, data...)
|
||||
if bytes.LastIndex(data, []byte{13}) == 0 {
|
||||
command := i.Command()
|
||||
i.Parser.Output.Listener.Reset()
|
||||
i.Parser.OutputData = nil
|
||||
i.Parser.InputData = nil
|
||||
if _, valid := i.CommandCheck(command); valid {
|
||||
if strings.TrimSpace(command) != "" {
|
||||
go i.Sshd.Core.Audit.AddCommand(model.SessionCmd{Cmd: command, Level: i.CommandLevel(command), SessionId: hostConn.SessionId})
|
||||
}
|
||||
if !write {
|
||||
_, err = hostConn.Stdin.Write(data)
|
||||
if err != nil {
|
||||
logger.L.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tips, _ := i.Localizer.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: myi18n.MsgSshCommandRefused,
|
||||
TemplateData: map[string]string{"Command": command},
|
||||
PluralCount: 1,
|
||||
})
|
||||
i.Parser.Ps2 = i.Parser.Ps1 + command
|
||||
targetOutputChan <- []byte("\r\n" + tips + i.Parser.Ps2)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "output": // output from target
|
||||
if !hostConn.Parser.State(data) {
|
||||
i.Parser.OutputData = append(i.Parser.OutputData, data...)
|
||||
}
|
||||
err = hostConn.Record.Write(data)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
Monitor(hostConn.SessionId, data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) Exits(conn *client.Connection) {
|
||||
if conn.GateWayCloseChan != nil {
|
||||
conn.GateWayCloseChan <- struct{}{}
|
||||
}
|
||||
_ = conn.Session.Close()
|
||||
conn.Record.Close()
|
||||
|
||||
config.TotalHostSession.Delete(conn.SessionId)
|
||||
config.TotalMonitors.Delete(conn.SessionId)
|
||||
|
||||
i.Locker.Lock()
|
||||
delete(i.SshSession, conn.SessionId)
|
||||
if len(i.SshSession) == 0 {
|
||||
_ = i.SshClient.Close()
|
||||
i.SshClient = nil
|
||||
}
|
||||
i.Locker.Unlock()
|
||||
err := i.UpsertSession(conn, model.SESSIONSTATUS_OFFLINE)
|
||||
if err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InteractiveHandler) CommandCheck(command string) (string, bool) {
|
||||
for _, id := range i.SelectedAsset.CmdIds {
|
||||
cmd, ok := i.Commands[id]
|
||||
if !ok || !cmd.Enable {
|
||||
continue
|
||||
}
|
||||
for _, c := range cmd.Cmds {
|
||||
p, err := regexp.Compile(c)
|
||||
if err == nil {
|
||||
if p.Match([]byte(command)) {
|
||||
return c, false
|
||||
}
|
||||
} else {
|
||||
if c == command {
|
||||
return c, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
//func (v *VirtualTermIn) Read(p []byte) (n int, err error) {
|
||||
// return v.InChan.Read(p)
|
||||
//}
|
||||
//
|
||||
//func (v *VirtualTermIn) Close() error {
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//func (v *VirtualTermOut) Write(p []byte) (n int, err error) {
|
||||
// return v.OutChan.Write(p)
|
||||
//}
|
||||
Reference in New Issue
Block a user