Commit e9f3361d authored by Eric's avatar Eric

use sync.map replace some native map container

parent 8f5d95aa
package main
import "cocogo/pkg/sshd"
import (
"cocogo/pkg/auth"
"cocogo/pkg/config"
"cocogo/pkg/sshd"
)
func main() {
var (
conf *config.Config
appService *auth.Service
)
func init() {
configFile := "config.yml"
conf = config.LoadFromYaml(configFile)
appService = auth.NewAuthService(conf)
appService.LoadAccessKey()
appService.EnsureValidAuth()
appService.LoadTerminalConfig()
sshd.Initial(conf, appService)
}
func main() {
sshd.StartServer()
}
......@@ -8,7 +8,10 @@ require (
github.com/gliderlabs/ssh v0.1.3
github.com/kr/pty v1.1.4 // indirect
github.com/mattn/go-runewidth v0.0.4
github.com/olekukonko/tablewriter v0.0.1
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.4.0
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576
gopkg.in/yaml.v2 v2.2.2
)
package asset
import "golang.org/x/crypto/ssh"
/*
{
"id": "060ba6be-a01d-41ef-b366-384b8a012274",
"hostname": "docker_test",
"ip": "127.0.0.1",
"port": 32768,
"system_users_granted": [
{
"id": "fbd39f8c-fa3e-4c2b-948e-ce1e0380b4f9",
"name": "docker_root",
"username": "root",
"priority": 20,
"protocol": "ssh",
"comment": "screencast",
"login_mode": "auto"
}
],
"is_active": true,
"system_users_join": "root",
"os": null,
"domain": null,
"platform": "Linux",
"comment": "screencast",
"protocol": "ssh",
"org_id": "",
"org_name": "DEFAULT"
}
*/
type Node struct {
IP string `json:"ip"`
Port string `json:"port"`
UserName string `json:"username"`
PassWord string `json:"password"`
PublicKey ssh.Signer
}
package auth
import "github.com/gliderlabs/ssh"
import "fmt"
type Service struct {
type accessAuth struct {
accessKey string
accessSecret string
}
var (
service = new(Service)
)
func NewService() *Service {
return service
}
func (s *Service) SSHPassword(ctx ssh.Context, password string) bool {
ctx.SessionID()
Username := "softwareuser1"
Password := "123456"
if ctx.User() == Username && password == Password {
return true
}
return false
func (a accessAuth) Signature(date string) string {
return fmt.Sprintf("Sign %s:%s", a.accessKey, MakeSignature(a.accessSecret, date))
}
package auth
import (
"bytes"
"cocogo/pkg/config"
"cocogo/pkg/model"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/gliderlabs/ssh"
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func NewAuthService(conf *config.Config) *Service {
return &Service{
http: &http.Client{},
Conf: conf,
}
}
type Service struct {
http *http.Client
Conf *config.Config
auth accessAuth
}
func (s *Service) CheckAuth(username, password, publicKey, remoteAddr, loginType string) (model.User, error) {
/*
{
'token': '0191970b1f5b414bbae42ec8fbb2a2ad',
'user':{'id': '34987591-bf75-4e5f-a102-6d59a1103431',
'name': 'softwareuser1', 'username': 'softwareuser1',
'email': 'xplz@hotmail.com',
'groups': ['bdc861f9-f476-4554-9bd4-13c3112e469d'],
'groups_display': '研发组', 'role': 'User',
'role_display': '用户', 'avatar_url': '/static/img/avatar/user.png',
'wechat': '', 'phone': None, 'otp_level': 0, 'comment': '',
'source': 'local', 'source_display': 'Local', 'is_valid': True,
'is_expired': False, 'is_active': True, 'created_by': 'admin',
'is_first_login': True, 'date_password_last_updated': '2019-03-08 11:47:04 +0800',
'date_expired': '2089-02-18 09:37:00 +0800'}}
*/
postMap := map[string]string{
"username": username,
"password": password,
"public_key": publicKey,
"remote_addr": remoteAddr,
"login_type": loginType,
}
data, err := json.Marshal(postMap)
if err != nil {
log.Info(err)
return model.User{}, err
}
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, UserAuthUrl)
body, err := s.SendHTTPRequest(http.MethodPost, url, data)
if err != nil {
log.Info("read body failed:", err)
return model.User{}, err
}
var result struct {
Token string `json:"token"`
User model.User `json:"user"`
}
err = json.Unmarshal(body, &result)
if err != nil {
log.Info("json decode failed:", err)
return model.User{}, err
}
return result.User, nil
}
func (s *Service) CheckSSHPassword(ctx ssh.Context, password string) bool {
username := ctx.User()
remoteAddr := ctx.RemoteAddr().String()
authUser, err := s.CheckAuth(username, password, "", remoteAddr, "T")
if err != nil {
return false
}
ctx.SetValue("LoginUser", authUser)
return true
}
func (s *Service) CheckSSHPublicKey(ctx ssh.Context, key ssh.PublicKey) bool {
username := ctx.User()
b := key.Marshal()
publicKeyBase64 := Base64Encode(string(b))
remoteAddr := ctx.RemoteAddr().String()
authUser, err := s.CheckAuth(username, "", publicKeyBase64, remoteAddr, "T")
if err != nil {
return false
}
ctx.SetValue("LoginUser", authUser)
return true
}
func (s *Service) EnsureValidAuth() {
for i := 0; i < 10; i++ {
if !s.getProfile() {
msg := `Connect server error or access key is invalid,
remove "./data/keys/.access_key" run again`
log.Error(msg)
time.Sleep(time.Second * 3)
} else {
break
}
if i == 3 {
os.Exit(1)
}
}
}
func (s *Service) LoadTerminalConfig() {
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, TerminalConfigUrl)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Info(err)
}
currentDate := HTTPGMTDate()
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Date", currentDate)
req.Header.Set("Authorization", s.auth.Signature(currentDate))
resp, err := s.http.Do(req)
if err != nil {
log.Info("client http request failed:", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Info("Read response Body err:", err)
return
}
fmt.Printf("%s\n", body)
resBody := config.TerminalConfig{}
err = json.Unmarshal(body, &resBody)
if err != nil {
log.Info("json.Unmarshal", err)
return
}
s.Conf.TermConfig = &resBody
fmt.Println(resBody)
}
func (s *Service) LoadAccessKey() {
/*
1. 查看配置文件是否包含accessKey,解析不正确则退出程序
2. 检查是否已经注册过accessKey,
1)已经注册过则进行解析,解析不正确则退出程序
2)未注册则新注册
*/
if s.Conf.CustomerAccessKey != "" {
fmt.Println(s.Conf.CustomerAccessKey)
keyAndSecret := strings.Split(s.Conf.CustomerAccessKey, ":")
if len(keyAndSecret) == 2 {
s.auth = accessAuth{
accessKey: keyAndSecret[0],
accessSecret: keyAndSecret[1],
}
} else {
fmt.Println("ACCESS_KEY format err")
os.Exit(1)
}
return
}
var configPath string
if !path.IsAbs(s.Conf.AccessKeyFile) {
configPath = filepath.Join(s.Conf.RootPath, s.Conf.AccessKeyFile)
} else {
configPath = s.Conf.AccessKeyFile
}
_, err := os.Stat(configPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("Do not have access key, register it!")
err := s.registerTerminalAndSave()
if err != nil {
log.Info("register Failed:", err)
os.Exit(1)
}
return
} else {
fmt.Println("sys err:", err)
os.Exit(1)
}
}
buf, err := ioutil.ReadFile(configPath)
if err != nil {
fmt.Println("read Access key Failed:", err)
os.Exit(1)
}
keyAndSecret := strings.Split(string(buf), ":")
if len(keyAndSecret) == 2 {
s.auth = accessAuth{
accessKey: keyAndSecret[0],
accessSecret: keyAndSecret[1],
}
} else {
fmt.Println("ACCESS_KEY format err")
os.Exit(1)
}
}
func (s *Service) registerTerminalAndSave() error {
postMap := map[string]string{
"name": s.Conf.Name,
"comment": s.Conf.Comment,
}
data, err := json.Marshal(postMap)
if err != nil {
log.Info("json encode failed:", err)
return err
}
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, TerminalRegisterUrl)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
log.Info("http NewRequest err:", err)
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("BootstrapToken %s", s.Conf.BootstrapToken))
resp, err := s.http.Do(req)
if err != nil {
log.Info("http request err:", err)
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Info("read resp body err:", err)
return err
}
/*
{
"name": "sss2",
"comment": "Coco",
"service_account": {
"id": "c2dece80-1811-42bc-bd5b-aef0f4180263",
"name": "sss2",
"access_key": {
"id": "f9b2cf91-7f30-45ea-9edf-b73ec0f48d5a",
"secret": "fd083b6c-e823-47bf-870c-0dd6051e69f1"
}
}
}
*/
log.Infof("%s", body)
var resBody struct {
ServiceAccount struct {
Id string `json:"id"`
Name string `json:"name"`
Accesskey struct {
Id string `json:"id"`
Secret string `json:"secret"`
} `json:"access_key"`
} `json:"service_account"`
}
err = json.Unmarshal(body, &resBody)
if err != nil {
log.Info("json Unmarshal:", err)
return err
}
if resBody.ServiceAccount.Name == "" {
return errors.New(string(body))
}
s.auth = accessAuth{
accessKey: resBody.ServiceAccount.Accesskey.Id,
accessSecret: resBody.ServiceAccount.Accesskey.Secret,
}
return s.saveAccessKey()
}
func (s *Service) saveAccessKey() error {
MakeSureDirExit(s.Conf.AccessKeyFile)
f, err := os.Create(s.Conf.AccessKeyFile)
fmt.Println("Create file path:", s.Conf.AccessKeyFile)
if err != nil {
return err
}
keyAndSecret := fmt.Sprintf("%s:%s", s.auth.accessKey, s.auth.accessSecret)
_, err = f.WriteString(keyAndSecret)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
return nil
}
func (s *Service) getProfile() bool {
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, UserProfileUrl)
body, err := s.SendHTTPRequest(http.MethodGet, url, nil)
if err != nil {
log.Info("Read response Body err:", err)
return false
}
result := model.User{}
err = json.Unmarshal(body, &result)
if err != nil {
log.Info("json.Unmarshal", err)
return false
}
log.Info(result)
return result != model.User{}
}
func (s *Service) SendHTTPRequest(method, url string, jsonData []byte) ([]byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
currentDate := HTTPGMTDate()
req.Header.Set("Date", currentDate)
req.Header.Set("Authorization", s.auth.Signature(currentDate))
resp, err := s.http.Do(req)
defer resp.Body.Close()
if err != nil {
log.Info("Send HTTP Request failed:", err)
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
return body, nil
}
func (s *Service) GetUserAssets(uid string) (resp []model.Asset, err error) {
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, fmt.Sprintf(UserAssetsUrl, uid))
buf, err := s.SendHTTPRequest("GET", url, nil)
if err != nil {
log.Info("get User Assets err:", err)
return resp, err
}
err = json.Unmarshal(buf, &resp)
if err != nil {
log.Info(err)
return resp, err
}
return resp, nil
}
func (s *Service) GetUserAssetNodes(uid string) ([]model.AssetNode, error) {
var resp []model.AssetNode
url := fmt.Sprintf("%s%s", s.Conf.CoreHost, fmt.Sprintf(UserNodesAssetsUrl, uid))
buf, err := s.SendHTTPRequest("GET", url, nil)
if err != nil {
log.Info("get User Assets Groups err:", err)
return resp, err
}
err = json.Unmarshal(buf, &resp)
if err != nil {
log.Info(err)
return resp, err
}
return resp, err
}
func (s *Service) GetSystemUserAssetAuthInfo(systemUserID, assetID string) (authInfo model.SystemUserAuthInfo, err error) {
url := fmt.Sprintf("%s%s", s.Conf.CoreHost,
fmt.Sprintf(SystemUserAssetAuthUrl, systemUserID, assetID))
buf, err := s.SendHTTPRequest("GET", url, nil)
if err != nil {
log.Info("get User Assets Groups err:", err)
return authInfo, err
}
err = json.Unmarshal(buf, &authInfo)
if err != nil {
log.Info(err)
return authInfo, err
}
return authInfo, err
}
func (s *Service) GetSystemUserAuthInfo(systemUserID string) {
url := fmt.Sprintf("%s%s", s.Conf.CoreHost,
fmt.Sprintf(SystemUserAuthUrl, systemUserID))
buf, err := s.SendHTTPRequest("GET", url, nil)
if err != nil {
log.Info("get User Assets Groups err:", err)
return
}
//err = json.Unmarshal(buf, &authInfo)
fmt.Printf("%s", buf)
if err != nil {
log.Info(err)
return
}
return
}
func (s *Service) ValidateUserAssetPermission(userID, systemUserID, AssetID string) bool {
// cache_policy 0:不使用缓存 1:使用缓存 2: 刷新缓存
baseUrl, _ := url.Parse(fmt.Sprintf("%s%s", s.Conf.CoreHost, ValidateUserAssetPermission))
params := url.Values{}
params.Add("user_id", userID)
params.Add("asset_id", AssetID)
params.Add("system_user_id", systemUserID)
params.Add("cache_policy", "1")
baseUrl.RawQuery = params.Encode()
buf, err := s.SendHTTPRequest("GET", baseUrl.String(), nil)
if err != nil {
log.Error("Check User Asset Permission err:", err)
return false
}
var res struct {
Msg bool `json:"msg"'`
}
if err = json.Unmarshal(buf, &res); err != nil {
return false
}
return res.Msg
}
package auth
import (
"crypto/md5"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
func HTTPGMTDate() string {
GmtDateLayout := "Mon, 02 Jan 2006 15:04:05 GMT"
return time.Now().UTC().Format(GmtDateLayout)
}
func MakeSignature(key, date string) string {
s := strings.Join([]string{key, date}, "\n")
return Base64Encode(MD5Encode([]byte(s)))
}
func Base64Encode(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}
func MD5Encode(b []byte) string {
return fmt.Sprintf("%x", md5.Sum(b))
}
func MakeSureDirExit(filePath string) {
dirPath := filepath.Dir(filePath)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
err = os.Mkdir(dirPath, os.ModePerm)
if err != nil {
log.Info("could not create dir path:", dirPath)
os.Exit(1)
}
log.Info("create dir path:", dirPath)
return
}
log.Info("dir path exits:", dirPath)
}
package auth
const (
TerminalRegisterUrl = "/api/terminal/v2/terminal-registrations/" // 注册当前coco
TerminalConfigUrl = "/api/terminal/v1/terminal/config/" // 从jumpserver获取coco的配置
UserAuthUrl = "/api/users/v1/auth/" // post 验证用户登陆
UserProfileUrl = "/api/users/v1/profile/" // 获取当前用户的基本信息
UserAssetsUrl = "/api/perms/v1/user/%s/assets/" //获取用户授权的所有资产
UserNodesAssetsUrl = "/api/perms/v1/user/%s/nodes-assets/" // 获取用户授权的所有节点信息 节点分组
SystemUserAssetAuthUrl = "/api/assets/v1/system-user/%s/asset/%s/auth-info/" // 该系统用户对某资产的授权
SystemUserAuthUrl = "/api/assets/v1/system-user/%s/auth-info/" // 该系统用户的授权
ValidateUserAssetPermission = "/api/perms/v1/asset-permission/user/validate/" //0不使用缓存 1 使用缓存 2 刷新缓存
)
/*
/api/assets/v1/system-user/%s/asset/%s/auth-info/
/api/assets/v1/system-user/fbd39f8c-fa3e-4c2b-948e-ce1e0380b4f9/cmd-filter-rules/
*/
package config
import (
"fmt"
"io/ioutil"
"os"
"gopkg.in/yaml.v2"
)
func LoadFromYaml(filepath string) *Config {
c := createDefaultConfig()
body, err := ioutil.ReadFile(filepath)
if err != nil {
os.Exit(1)
}
e := yaml.Unmarshal(body, &c)
if e != nil {
fmt.Println("load yaml err")
os.Exit(1)
}
return &c
}
func createDefaultConfig() Config {
name, _ := os.Hostname()
rootPath, _ := os.Getwd()
return Config{
Name: name,
CoreHost: "http://localhost:8080",
BootstrapToken: "",
BindHost: "0.0.0.0",
SshPort: 2222,
HTTPPort: 5000,
CustomerAccessKey: "",
AccessKeyFile: "data/keys/.access_key",
LogLevel: "DEBUG",
RootPath: rootPath,
Comment: "Coco",
TermConfig: &TerminalConfig{},
}
}
type Config struct {
Name string `yaml:"NAME"`
CoreHost string `yaml:"CORE_HOST"`
BootstrapToken string `yaml:"BOOTSTRAP_TOKEN"`
BindHost string `yaml:"BIND_HOST"`
SshPort int `yaml:"SSHD_PORT"`
HTTPPort int `yaml:"HTTPD_PORT"`
CustomerAccessKey string `yaml:"ACCESS_KEY"`
AccessKeyFile string `yaml:"ACCESS_KEY_FILE"`
LogLevel string `yaml:"LOG_LEVEL"`
HeartBeat int `yaml:"HEARTBEAT_INTERVAL"`
RootPath string
Comment string
TermConfig *TerminalConfig
}
type TerminalConfig struct {
AssetListPageSize string `json:"TERMINAL_ASSET_LIST_PAGE_SIZE"`
AssetListSortBy string `json:"TERMINAL_ASSET_LIST_SORT_BY"`
CommandStorage Storage `json:"TERMINAL_COMMAND_STORAGE"`
HeaderTitle string `json:"TERMINAL_HEADER_TITLE"`
HeartBeatInterval int `json:"TERMINAL_HEARTBEAT_INTERVAL"`
HostKey string `json:"TERMINAL_HOST_KEY"`
PasswordAuth bool `json:"TERMINAL_PASSWORD_AUTH"`
PublicKeyAuth bool `json:"TERMINAL_PUBLIC_KEY_AUTH"`
RePlayStorage Storage `json:"TERMINAL_REPLAY_STORAGE"`
SessionKeepDuration int `json:"TERMINAL_SESSION_KEEP_DURATION"`
TelnetRegex string `json:"TERMINAL_TELNET_REGEX"`
SecurityMaxIdleTime int `json:"SECURITY_MAX_IDLE_TIME"`
}
type Storage struct {
Type string `json:"TYPE"`
}
......@@ -5,13 +5,66 @@ import (
"fmt"
"io"
"github.com/gliderlabs/ssh"
uuid "github.com/satori/go.uuid"
gossh "golang.org/x/crypto/ssh"
)
func NewNodeConn(c *gossh.Client, s *gossh.Session, useS Conn) (*NodeConn, error) {
ptyReq, winCh, _ := useS.Pty()
err := s.RequestPty(ptyReq.Term, ptyReq.Window.Height, ptyReq.Window.Width, gossh.TerminalModes{})
type Conn interface {
SessionID() string
User() string
UUID() uuid.UUID
Pty() (ssh.Pty, <-chan ssh.Window, bool)
Context() context.Context
io.Reader
io.WriteCloser
}
type ServerAuth struct {
IP string
Port int
UserName string
Password string
PublicKey gossh.Signer
}
func CreateNodeSession(authInfo ServerAuth) (c *gossh.Client, s *gossh.Session, err error) {
config := &gossh.ClientConfig{
User: authInfo.UserName,
Auth: []gossh.AuthMethod{
gossh.Password(authInfo.Password),
gossh.PublicKeys(authInfo.PublicKey),
},
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}
client, err := gossh.Dial("tcp", fmt.Sprintf("%s:%d", authInfo.IP, authInfo.Port), config)
if err != nil {
log.Error(err)
return c, s, err
}
s, err = client.NewSession()
if err != nil {
log.Error(err)
return c, s, err
}
return client, s, nil
}
func NewNodeConn(authInfo ServerAuth, userS Conn) (*NodeConn, error) {
c, s, err := CreateNodeSession(authInfo)
if err != nil {
return nil, err
}
ptyReq, winCh, _ := userS.Pty()
err = s.RequestPty(ptyReq.Term, ptyReq.Window.Height, ptyReq.Window.Width, gossh.TerminalModes{})
if err != nil {
return nil, err
}
......@@ -28,58 +81,22 @@ func NewNodeConn(c *gossh.Client, s *gossh.Session, useS Conn) (*NodeConn, error
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(useS.Context())
Out, In := io.Pipe()
go func() {
for {
select {
case <-ctx.Done():
log.Info("NewNodeConn goroutine closed")
err = s.Close()
err = c.Close()
if err != io.EOF && err != nil {
log.Info(" sess Close():", err)
}
return
case win, ok := <-winCh:
if !ok {
return
}
err = s.WindowChange(win.Height, win.Width)
if err != nil {
log.Info("windowChange err: ", win)
return
}
log.Info("windowChange: ", win)
}
}
}()
ctx, cancelFunc := context.WithCancel(userS.Context())
go func() {
nr, err := io.Copy(In, nodeStdout)
if err != nil {
log.Info("io copy err:", err)
}
err = In.Close()
if err != nil {
log.Info("io copy c.Close():", err)
}
cancel()
log.Info("io copy int:", nr)
}()
nConn := &NodeConn{
uuid: uuid.NewV4(),
client: c,
conn: s,
stdin: nodeStdin,
stdout: nodeStdout,
cusOut: Out,
cusIn: In,
tParser: NewTerminalParser(),
uuid: uuid.NewV4(),
client: c,
conn: s,
ctx: ctx,
ctxCancelFunc: cancelFunc,
stdin: nodeStdin,
stdout: nodeStdout,
tParser: NewTerminalParser(),
inChan: make(chan []byte),
outChan: make(chan []byte),
}
go nConn.windowChangeHandler(winCh)
return nConn, nil
}
......@@ -90,90 +107,22 @@ type NodeConn struct {
conn *gossh.Session
stdin io.Writer
stdout io.Reader
cusIn io.WriteCloser
cusOut io.ReadCloser
tParser *TerminalParser
currentCommandInput string
currentCommandResult string
rulerFilters []RuleFilter
specialCommands []SpecialRuler
inSpecialStatus bool
ctx context.Context
ctxCancelFunc context.CancelFunc
inChan chan []byte
outChan chan []byte
}
func (n *NodeConn) UUID() uuid.UUID {
return n.uuid
}
func (n *NodeConn) Read(b []byte) (nr int, err error) {
nr, err = n.cusOut.Read(b)
if n.tParser.Started && nr > 0 {
n.FilterSpecialCommand(b[:nr])
switch {
case n.inSpecialStatus:
// 进入特殊命令状态,
case n.tParser.InputStatus:
n.tParser.CmdInputBuf.Write(b[:nr])
case n.tParser.OutputStatus:
n.tParser.CmdOutputBuf.Write(b[:nr])
default:
}
}
return nr, err
}
func (n *NodeConn) Write(b []byte) (nw int, err error) {
n.tParser.Once.Do(
func() {
n.tParser.Started = true
})
switch {
case n.inSpecialStatus:
// 特殊的命令 vim 或者 rz
case n.tParser.IsEnterKey(b):
n.currentCommandInput = n.tParser.ParseCommandInput()
if n.FilterWhiteBlackRule(n.currentCommandInput) {
msg := fmt.Sprintf("\r\n cmd '%s' is forbidden \r\n", n.currentCommandInput)
nw, err = n.cusIn.Write([]byte(msg))
if err != nil {
return nw, err
}
ctrU := []byte{21, 13} // 清除行并换行
nw, err = n.stdin.Write(ctrU)
if err != nil {
return nw, err
}
n.tParser.InputStatus = false
n.tParser.OutputStatus = false
return len(b), nil
}
n.tParser.InputStatus = false
n.tParser.OutputStatus = true
default:
// 1. 是否是一个命令的完整周期 是则解析命令,记录结果 并重置
// 2. 重置用户输入状态
if len(n.tParser.CmdOutputBuf.Bytes()) > 0 && n.currentCommandInput != "" {
n.currentCommandResult = n.tParser.ParseCommandResult()
n.tParser.Reset()
n.currentCommandInput = ""
n.currentCommandResult = ""
}
n.tParser.InputStatus = true
}
return n.stdin.Write(b)
}
func (n *NodeConn) Close() error {
return n.cusOut.Close()
}
func (n *NodeConn) Wait() error {
return n.conn.Wait()
}
......@@ -200,3 +149,126 @@ func (n *NodeConn) FilterWhiteBlackRule(cmd string) bool {
}
return false
}
func (n *NodeConn) windowChangeHandler(winCH <-chan ssh.Window) {
for {
select {
case <-n.ctx.Done():
log.Info("windowChangeHandler done")
return
case win, ok := <-winCH:
if !ok {
return
}
err := n.conn.WindowChange(win.Height, win.Width)
if err != nil {
log.Error("windowChange err: ", win)
return
}
log.Info("windowChange: ", win)
}
}
}
func (n *NodeConn) handleRequest(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case buf, ok := <-n.inChan:
if !ok {
return
}
n.tParser.Once.Do(
func() {
n.tParser.Started = true
})
switch {
case n.inSpecialStatus:
// 特殊的命令 vim 或者 rz
case n.tParser.IsEnterKey(buf):
n.currentCommandInput = n.tParser.ParseCommandInput()
if n.FilterWhiteBlackRule(n.currentCommandInput) {
msg := fmt.Sprintf("\r\n cmd '%s' is forbidden \r\n", n.currentCommandInput)
n.outChan <- []byte(msg)
ctrU := []byte{21, 13} // 清除行并换行
_, err := n.stdin.Write(ctrU)
if err != nil {
log.Error(err)
}
n.tParser.InputStatus = false
n.tParser.OutputStatus = false
continue
}
n.tParser.InputStatus = false
n.tParser.OutputStatus = true
default:
// 1. 是否是一个命令的完整周期 是则解析命令,记录结果 并重置
// 2. 重置用户输入状态
if len(n.tParser.CmdOutputBuf.Bytes()) > 0 && n.currentCommandInput != "" {
n.currentCommandResult = n.tParser.ParseCommandResult()
n.tParser.Reset()
n.currentCommandInput = ""
n.currentCommandResult = ""
}
n.tParser.InputStatus = true
}
_, _ = n.stdin.Write(buf)
}
}
}
func (n *NodeConn) handleResponse(ctx context.Context) {
buf := make([]byte, maxBufferSize)
defer close(n.outChan)
for {
nr, err := n.stdout.Read(buf)
if err != nil {
return
}
if n.tParser.Started && nr > 0 {
n.FilterSpecialCommand(buf[:nr])
switch {
case n.inSpecialStatus:
// 进入特殊命令状态,
case n.tParser.InputStatus:
n.tParser.CmdInputBuf.Write(buf[:nr])
case n.tParser.OutputStatus:
n.tParser.CmdOutputBuf.Write(buf[:nr])
default:
}
}
select {
case <-ctx.Done():
return
default:
copyBuf := make([]byte, len(buf[:nr]))
copy(copyBuf, buf[:nr])
n.outChan <- copyBuf
}
}
}
func (n *NodeConn) Close() {
select {
case <-n.ctx.Done():
return
default:
_ = n.conn.Close()
_ = n.client.Close()
n.ctxCancelFunc()
}
}
......@@ -15,10 +15,11 @@ type ProxyChannel interface {
Wait() error
}
func NewMemoryChannel(n *NodeConn) *memoryChannel {
func NewMemoryChannel(nConn *NodeConn, useS Conn) *memoryChannel {
m := &memoryChannel{
uuid: n.UUID(),
conn: n,
uuid: nConn.UUID(),
conn: nConn,
}
return m
}
......@@ -33,58 +34,13 @@ func (m *memoryChannel) UUID() string {
}
func (m *memoryChannel) SendResponseChannel(ctx context.Context) <-chan []byte {
// 传入context, 可以从外层进行取消
sendChannel := make(chan []byte)
go func() {
defer close(sendChannel)
resp := make([]byte, maxBufferSize)
for {
nr, e := m.conn.Read(resp)
if e != nil {
log.Info("m.conn.Read(resp) err: ", e)
break
}
select {
case <-ctx.Done():
return
default:
sendChannel <- resp[:nr]
}
}
}()
return sendChannel
go m.conn.handleResponse(ctx)
return m.conn.outChan
}
func (m *memoryChannel) ReceiveRequestChannel(ctx context.Context) chan<- []byte {
// 传入context, 可以从外层进行取消
receiveChannel := make(chan []byte)
go func() {
defer m.conn.Close()
for {
select {
case <-ctx.Done():
log.Info("ReceiveRequestChannel ctx done")
return
case reqBuf, ok := <-receiveChannel:
if !ok {
return
}
nw, e := m.conn.Write(reqBuf)
if e != nil && nw != len(reqBuf) {
return
}
}
}
}()
return receiveChannel
go m.conn.handleRequest(ctx)
return m.conn.inChan
}
func (m *memoryChannel) Wait() error {
......
......@@ -5,95 +5,76 @@ import (
"sync"
)
type room struct {
sessionID string
uHome SessionHome
pChan ProxyChannel
var Manager = &manager{
container: new(sync.Map),
}
var Manager = &manager{container: map[string]room{}}
type manager struct {
container map[string]room
sync.RWMutex
container *sync.Map
}
func (m *manager) add(uHome SessionHome, pChan ProxyChannel) {
m.Lock()
m.container[uHome.SessionID()] = room{
sessionID: uHome.SessionID(),
uHome: uHome,
pChan: pChan,
}
m.Unlock()
func (m *manager) add(uHome SessionHome) {
m.container.Store(uHome.SessionID(), uHome)
}
func (m *manager) delete(roomID string) {
m.Lock()
delete(m.container, roomID)
m.Unlock()
m.container.Delete(roomID)
}
func (m *manager) search(roomID string) (SessionHome, bool) {
m.RLock()
defer m.RUnlock()
if room, ok := m.container[roomID]; ok {
return room.uHome, ok
if uHome, ok := m.container.Load(roomID); ok {
return uHome.(SessionHome), ok
}
return nil, false
}
func JoinShareRoom(roomID string, uConn Conn) {
if userHome, ok := Manager.search(roomID); ok {
func (m *manager) JoinShareRoom(roomID string, uConn Conn) {
if userHome, ok := m.search(roomID); ok {
userHome.AddConnection(uConn)
}
}
func ExitShareRoom(roomID string, uConn Conn) {
if userHome, ok := Manager.search(roomID); ok {
func (m *manager) ExitShareRoom(roomID string, uConn Conn) {
if userHome, ok := m.search(roomID); ok {
userHome.RemoveConnection(uConn)
}
}
func Switch(ctx context.Context, userHome SessionHome, pChannel ProxyChannel) error {
Manager.add(userHome, pChannel)
defer Manager.delete(userHome.SessionID())
subCtx, cancel := context.WithCancel(ctx)
var wg sync.WaitGroup
wg.Add(2)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
userSendRequestStream := userHome.SendRequestChannel(ctx)
nodeRequestChan := pChannel.ReceiveRequestChannel(ctx)
for reqFromUser := range userSendRequestStream {
nodeRequestChan <- reqFromUser
func (m *manager) Switch(ctx context.Context, userHome SessionHome, pChannel ProxyChannel) error {
m.add(userHome)
defer m.delete(userHome.SessionID())
subCtx, cancelFunc := context.WithCancel(ctx)
userSendRequestStream := userHome.SendRequestChannel(subCtx)
userReceiveStream := userHome.ReceiveResponseChannel(subCtx)
nodeRequestChan := pChannel.ReceiveRequestChannel(subCtx)
nodeSendResponseStream := pChannel.SendResponseChannel(subCtx)
for userSendRequestStream != nil || nodeSendResponseStream != nil {
select {
case buf1, ok := <-userSendRequestStream:
if !ok {
log.Warn("userSendRequestStream close")
userSendRequestStream = nil
continue
}
nodeRequestChan <- buf1
case buf2, ok := <-nodeSendResponseStream:
if !ok {
log.Warn("nodeSendResponseStream close")
nodeSendResponseStream = nil
close(userReceiveStream)
cancelFunc()
continue
}
userReceiveStream <- buf2
case <-ctx.Done():
return nil
}
log.Info("userSendRequestStream close")
close(nodeRequestChan)
}(subCtx, &wg)
go func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
userReceiveStream := userHome.ReceiveResponseChannel(ctx)
nodeSendResponseStream := pChannel.SendResponseChannel(ctx)
for resFromNode := range nodeSendResponseStream {
userReceiveStream <- resFromNode
}
log.Info("nodeSendResponseStream close")
close(userReceiveStream)
}(subCtx, &wg)
err := pChannel.Wait()
if err != nil {
log.Info("pChannel err:", err)
}
cancel()
wg.Wait()
log.Info("switch end")
return err
return nil
}
......@@ -2,28 +2,9 @@ package core
import (
"context"
"io"
"sync"
"github.com/gliderlabs/ssh"
uuid "github.com/satori/go.uuid"
)
type Conn interface {
SessionID() string
User() string
UUID() uuid.UUID
Pty() (ssh.Pty, <-chan ssh.Window, bool)
Context() context.Context
io.Reader
io.WriteCloser
}
type SessionHome interface {
SessionID() string
AddConnection(c Conn)
......@@ -33,21 +14,22 @@ type SessionHome interface {
}
func NewUserSessionHome(con Conn) *userSessionHome {
return &userSessionHome{
uHome := &userSessionHome{
readStream: make(chan []byte),
mainConn: con,
connMap: map[string]Conn{con.UUID().String(): con},
cancelMap: map[string]context.CancelFunc{},
connMap: new(sync.Map),
cancelMap: new(sync.Map),
}
uHome.connMap.Store(con.SessionID(), con)
return uHome
}
type userSessionHome struct {
readStream chan []byte
mainConn Conn
connMap map[string]Conn
cancelMap map[string]context.CancelFunc
sync.RWMutex
connMap *sync.Map
cancelMap *sync.Map
}
func (r *userSessionHome) SessionID() string {
......@@ -57,9 +39,9 @@ func (r *userSessionHome) SessionID() string {
func (r *userSessionHome) AddConnection(c Conn) {
key := c.SessionID()
if _, ok := r.connMap[key]; !ok {
if _, ok := r.connMap.Load(key); !ok {
log.Info("add connection ", c)
r.connMap[key] = c
r.connMap.Store(key, c)
} else {
log.Info("already add connection")
return
......@@ -68,7 +50,7 @@ func (r *userSessionHome) AddConnection(c Conn) {
log.Info("add conn session room: ", r.SessionID())
ctx, cancelFunc := context.WithCancel(r.mainConn.Context())
r.cancelMap[key] = cancelFunc
r.cancelMap.Store(key, cancelFunc)
defer r.RemoveConnection(c)
......@@ -82,10 +64,12 @@ func (r *userSessionHome) AddConnection(c Conn) {
select {
case <-ctx.Done():
log.Info("conn ctx done")
log.Info(" user conn ctx done")
return
default:
r.readStream <- buf[:nr]
copyBuf := make([]byte, nr)
copy(copyBuf, buf[:nr])
r.readStream <- copyBuf
}
......@@ -94,13 +78,13 @@ func (r *userSessionHome) AddConnection(c Conn) {
}
func (r *userSessionHome) RemoveConnection(c Conn) {
r.Lock()
defer r.Unlock()
key := c.SessionID()
if _, ok := r.connMap[key]; ok {
delete(r.connMap, key)
delete(r.cancelMap, key)
if cancelFunc, ok := r.cancelMap.Load(key); ok {
cancelFunc.(context.CancelFunc)()
}
r.connMap.Delete(key)
}
func (r *userSessionHome) SendRequestChannel(ctx context.Context) <-chan []byte {
......@@ -118,7 +102,9 @@ func (r *userSessionHome) SendRequestChannel(ctx context.Context) <-chan []byte
case <-ctx.Done():
return
default:
r.readStream <- buf[:nr]
var respCopy []byte
respCopy = append(respCopy, buf[:nr]...)
r.readStream <- respCopy
}
}
......@@ -132,11 +118,10 @@ func (r *userSessionHome) ReceiveResponseChannel(ctx context.Context) chan<- []b
writeStream := make(chan []byte)
go func() {
defer func() {
r.RLock()
for _, cancel := range r.cancelMap {
cancel()
}
r.RUnlock()
r.cancelMap.Range(func(key, cancelFunc interface{}) bool {
cancelFunc.(context.CancelFunc)()
return true
})
}()
for {
......@@ -147,13 +132,15 @@ func (r *userSessionHome) ReceiveResponseChannel(ctx context.Context) chan<- []b
if !ok {
return
}
for _, c := range r.connMap {
nw, err := c.Write(buf)
r.connMap.Range(func(key, connItem interface{}) bool {
nw, err := connItem.(Conn).Write(buf)
if err != nil || nw != len(buf) {
log.Error("Write Conn err", c)
r.cancelMap[c.SessionID()]()
log.Error("Write Conn err", connItem)
r.RemoveConnection(connItem.(Conn))
}
}
return true
})
}
}
......
......@@ -23,17 +23,17 @@ type Rule struct {
action bool
}
func (w *Rule) Match(s string) bool {
switch w.ruleType {
func (r *Rule) Match(s string) bool {
switch r.ruleType {
case "command":
for _, content := range w.contents {
for _, content := range r.contents {
if content == s {
return true
}
}
return false
default:
for _, content := range w.contents {
for _, content := range r.contents {
if matched, _ := regexp.MatchString(content, s); matched {
return true
}
......@@ -43,6 +43,6 @@ func (w *Rule) Match(s string) bool {
}
func (w *Rule) BlockCommand() bool {
return w.action
func (r *Rule) BlockCommand() bool {
return r.action
}
package model
/*
{
"id": "135ce78d-c4fe-44ca-9be3-c86581cb4365",
"hostname": "coco2",
"ip": "127.0.0.1",
"port": 32769,
"system_users_granted": [{
"id": "fbd39f8c-fa3e-4c2b-948e-ce1e0380b4f9",
"name": "docker_root",
"username": "root",
"priority": 19,
"protocol": "ssh",
"comment": "screencast",
"login_mode": "auto"
}],
"is_active": true,
"system_users_join": "root",
"os": null,
"domain": null,
"platform": "Linux",
"comment": "",
"protocol": "ssh",
"org_id": "",
"org_name": "DEFAULT"
}
*/
type Asset struct {
Id string `json:"id"`
Hostname string `json:"hostname"`
Ip string `json:"ip"`
Port int `json:"port"`
SystemUsers []SystemUser `json:"system_users_granted"`
IsActive bool `json:"is_active"`
SystemUsersJoin string `json:"system_users_join"`
Os string `json:"os"`
Domain string `json:"domain"`
Platform string `json:"platform"`
Comment string `json:"comment"`
Protocol string `json:"protocol"`
OrgID string `json:"org_id"`
OrgName string `json:"org_name"`
}
package model
import (
"sort"
"strconv"
"strings"
)
type AssetNode struct {
Id string `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
Value string `json:"value"`
Parent string `json:"parent"`
AssetsGranted []Asset `json:"assets_granted"`
AssetsAmount int `json:"assets_amount"`
OrgId string `json:"org_id"`
}
type nodeSortBy func(node1, node2 *AssetNode) bool
func (by nodeSortBy) Sort(assetNodes []AssetNode) {
nodeSorter := &AssetNodeSorter{
assetNodes: assetNodes,
sortBy: by,
}
sort.Sort(nodeSorter)
}
type AssetNodeSorter struct {
assetNodes []AssetNode
sortBy func(node1, node2 *AssetNode) bool
}
func (a *AssetNodeSorter) Len() int {
return len(a.assetNodes)
}
func (a *AssetNodeSorter) Swap(i, j int) {
a.assetNodes[i], a.assetNodes[j] = a.assetNodes[j], a.assetNodes[i]
}
func (a *AssetNodeSorter) Less(i, j int) bool {
return a.sortBy(&a.assetNodes[i], &a.assetNodes[j])
}
/*
key的排列顺序:
1 1:3 1:3:0 1:4 1:5 1:8
*/
func keySort(node1, node2 *AssetNode) bool {
node1Keys := strings.Split(node1.Key, ":")
node2Keys := strings.Split(node2.Key, ":")
for i := 0; i < len(node1Keys); i++ {
if i >= len(node2Keys) {
return false
}
node1num, _ := strconv.Atoi(node1Keys[i])
node2num, _ := strconv.Atoi(node2Keys[i])
if node1num == node2num {
continue
} else if node1num-node2num > 0 {
return false
} else {
return true
}
}
return true
}
func SortAssetNodesByKey(assetNodes []AssetNode) {
nodeSortBy(keySort).Sort(assetNodes)
}
package model
import (
"sort"
)
/*
{"id": "fbd39f8c-fa3e-4c2b-948e-ce1e0380b4f9",
"name": "docker_root",
"username": "root",
"priority": 19,
"protocol": "ssh",
"comment": "screencast",
"login_mode": "auto"}
*/
type SystemUser struct {
Id string `json:"id"`
Name string `json:"name"`
UserName string `json:"username"`
Priority int `json:"priority"`
Protocol string `json:"protocol"`
Comment string `json:"comment"`
LoginMode string `json:"login_mode"`
}
type SystemUserAuthInfo struct {
Id string `json:"id"`
Name string `json:"name"`
UserName string `json:"username"`
Protocol string `json:"protocol"`
LoginMode string `json:"login_mode"`
Password string `json:"password"`
PrivateKey string `json:"private_key"`
}
type systemUserSortBy func(user1, user2 *SystemUser) bool
func (by systemUserSortBy) Sort(users []SystemUser) {
nodeSorter := &systemUserSorter{
users: users,
sortBy: by,
}
sort.Sort(nodeSorter)
}
type systemUserSorter struct {
users []SystemUser
sortBy func(user1, user2 *SystemUser) bool
}
func (s *systemUserSorter) Len() int {
return len(s.users)
}
func (s *systemUserSorter) Swap(i, j int) {
s.users[i], s.users[j] = s.users[j], s.users[i]
}
func (s *systemUserSorter) Less(i, j int) bool {
return s.sortBy(&s.users[i], &s.users[j])
}
func systemUserPrioritySort(use1, user2 *SystemUser) bool {
return use1.Priority <= user2.Priority
}
func SortSystemUserByPriority(users []SystemUser) {
systemUserSortBy(systemUserPrioritySort).Sort(users)
}
package model
/*
{'id': '1f8e54a8-d99d-4074-b35d-45264adb4e34',
'name': 'EricdeMBP.lan',
'username': 'EricdeMBP.lan',
'email': 'EricdeMBP.lan@serviceaccount.local',
'groups': [],
'groups_display': '',
'role': 'App','role_display': '应用程序',
'avatar_url': '/static/img/avatar/user.png',
'wechat': '','phone': None, 'otp_level': 0,
'comment': '', 'source': 'local',
'source_display': 'Local',
'is_valid': True, 'is_expired': False,
'is_active': True, 'created_by': '',
'is_first_login': True, 'date_password_last_updated': '2019-04-08 18:18:24 +0800',
'date_expired': '2089-03-21 18:18:24 +0800'}
*/
type User struct {
Id string `json:"id"`
Name string `json:"name"`
UserName string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
IsValid bool `json:"is_valid"`
IsActive bool `json:"is_active"`
}
package sshd
const (
GreenColorCode = "\033[32m"
ColorEnd = "\033[0m"
Tab = "\t"
EndLine = "\r\n\r"
)
const (
AssetsMapKey = "AssetsMapKey"
AssetNodesMapKey = "AssetNodesKey"
)
package sshd
import (
"cocogo/pkg/asset"
"cocogo/pkg/core"
"cocogo/pkg/model"
"context"
"fmt"
"io"
"runtime"
"strconv"
"strings"
"sync"
"github.com/olekukonko/tablewriter"
"github.com/xlab/treeprint"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
const welcomeTemplate = `
{{.UserName}} Welcome to use Jumpserver open source fortress system
{{.Tab}}1) Enter {{.ColorCode}}ID{{.ColorEnd}} directly login or enter {{.ColorCode}}part IP, Hostname, Comment{{.ColorEnd}} to search login(if unique). {{.EndLine}}
{{.Tab}}2) Enter {{.ColorCode}}/{{.ColorEnd}} + {{.ColorCode}}IP, Hostname{{.ColorEnd}} or {{.ColorCode}}Comment{{.ColorEnd}} search, such as: /ip. {{.EndLine}}
{{.Tab}}3) Enter {{.ColorCode}}p{{.ColorEnd}} to display the host you have permission.{{.EndLine}}
{{.Tab}}4) Enter {{.ColorCode}}g{{.ColorEnd}} to display the node that you have permission.{{.EndLine}}
{{.Tab}}5) Enter {{.ColorCode}}g{{.ColorEnd}} + {{.ColorCode}}NodeID{{.ColorEnd}} to display the host under the node, such as g1. {{.EndLine}}
{{.Tab}}6) Enter {{.ColorCode}}s{{.ColorEnd}} Chinese-english switch.{{.EndLine}}
{{.Tab}}7) Enter {{.ColorCode}}h{{.ColorEnd}} help.{{.EndLine}}
{{.Tab}}8) Enter {{.ColorCode}}r{{.ColorEnd}} to refresh your assets and nodes.{{.EndLine}}
{{.Tab}}0) Enter {{.ColorCode}}q{{.ColorEnd}} exit.{{.EndLine}}
`
type HelpInfo struct {
UserName string
ColorCode string
......@@ -42,142 +33,326 @@ func (d HelpInfo) displayHelpInfo(sess ssh.Session) {
}
}
func InteractiveHandler(sess ssh.Session) {
_, winCh, ptyOk := sess.Pty()
if ptyOk {
helpInfo := HelpInfo{
UserName: sess.User(),
ColorCode: "\033[32m",
ColorEnd: "\033[0m",
Tab: "\t",
EndLine: "\r\n\r",
}
type sshInteractive struct {
sess ssh.Session
term *terminal.Terminal
assetData sync.Map
user model.User
helpInfo HelpInfo
currentSearchAssets []model.Asset
onceLoad sync.Once
sync.RWMutex
}
log.Info("accept one session")
helpInfo.displayHelpInfo(sess)
term := terminal.NewTerminal(sess, "Opt>")
func (s *sshInteractive) displayHelpInfo() {
s.helpInfo.displayHelpInfo(s.sess)
}
for {
fmt.Println("start g num:", runtime.NumGoroutine())
ctx, cancelFuc := context.WithCancel(sess.Context())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("ctx done")
return
case win, ok := <-winCh:
if !ok {
return
}
fmt.Println("InteractiveHandler term change:", win)
_ = term.SetSize(win.Width, win.Height)
}
}
}()
line, err := term.ReadLine()
cancelFuc()
if err != nil {
log.Error("ReadLine done", err)
break
func (s *sshInteractive) chooseSystemUser(systemUsers []model.SystemUser) model.SystemUser {
table := tablewriter.NewWriter(s.sess)
table.SetHeader([]string{"ID", "UserName"})
for i := 0; i < len(systemUsers); i++ {
table.Append([]string{strconv.Itoa(i + 1), systemUsers[i].UserName})
}
table.SetBorder(false)
count := 0
term := terminal.NewTerminal(s.sess, "num:")
for count < 3 {
table.Render()
line, err := term.ReadLine()
if err != nil {
continue
}
if num, err := strconv.Atoi(line); err == nil {
if num > 0 && num <= len(systemUsers) {
return systemUsers[num-1]
}
switch line {
case "p", "P":
_, err := io.WriteString(sess, "p cmd execute\r\n")
if err != nil {
return
}
case "g", "G":
_, err := io.WriteString(sess, "g cmd execute\r\n")
if err != nil {
return
}
case "s", "S":
_, err := io.WriteString(sess, "s cmd execute\r\n")
if err != nil {
return
}
case "h", "H":
helpInfo.displayHelpInfo(sess)
}
count++
}
return systemUsers[0]
}
case "r", "R":
_, err := io.WriteString(sess, "r cmd execute\r\n")
if err != nil {
return
}
case "q", "Q", "exit", "quit":
log.Info("exit session")
return
default:
searchNodeAndProxy(line, sess)
fmt.Println("end g num:", runtime.NumGoroutine())
}
// 当资产的数量为1的时候,就进行代理转化
func (s *sshInteractive) displayAssetsOrProxy(assets []model.Asset) {
if len(assets) == 1 {
var systemUser model.SystemUser
switch len(assets[0].SystemUsers) {
case 0:
// 有授权的资产,但是资产用户信息,无法登陆
s.displayAssets(assets)
return
case 1:
systemUser = assets[0].SystemUsers[0]
default:
systemUser = s.chooseSystemUser(assets[0].SystemUsers)
}
} else {
_, err := io.WriteString(sess, "No PTY requested.\n")
authInfo, err := appService.GetSystemUserAssetAuthInfo(systemUser.Id, assets[0].Id)
if err != nil {
return
}
if ok := appService.ValidateUserAssetPermission(s.user.Id, systemUser.Id, assets[0].Id); !ok {
// 检查user 是否对该资产有权限
return
}
err = s.Proxy(assets[0], authInfo)
if err != nil {
log.Info(err)
}
return
} else {
s.displayAssets(assets)
}
}
func (s *sshInteractive) displayAssets(assets []model.Asset) {
if len(assets) == 0 {
_, _ = io.WriteString(s.sess, "\r\n No Assets\r\n\r")
} else {
table := tablewriter.NewWriter(s.sess)
table.SetHeader([]string{"ID", "Hostname", "IP", "LoginAs", "Comment"})
for index, assetItem := range assets {
sysUserArray := make([]string, len(assetItem.SystemUsers))
for index, sysUser := range assetItem.SystemUsers {
sysUserArray[index] = sysUser.Name
}
sysUsers := "[" + strings.Join(sysUserArray, " ") + "]"
table.Append([]string{strconv.Itoa(index + 1), assetItem.Hostname, assetItem.Ip, sysUsers, assetItem.Comment})
}
table.SetBorder(false)
table.Render()
}
}
func (s *sshInteractive) displayAssetNodes(nodes []model.AssetNode) {
tree := ConstructAssetNodeTree(nodes)
tipHeaderMsg := "\r\nNode: [ ID.Name(Asset amount) ]"
tipEndMsg := "Tips: Enter g+NodeID to display the host under the node, such as g1\r\n\r"
_, err := io.WriteString(s.sess, tipHeaderMsg)
_, err = io.WriteString(s.sess, tree.String())
_, err = io.WriteString(s.sess, tipEndMsg)
if err != nil {
log.Info("displayAssetNodes err:", err)
}
}
func searchNodeAndProxy(line string, sess ssh.Session) {
searchWord := strings.TrimPrefix(line, "/")
if strings.Contains(searchWord, "join") {
func (s *sshInteractive) refreshAssetsAndNodesData() {
s.loadUserAssets()
s.loadUserAssetNodes()
_, err := io.WriteString(s.sess, "Refresh done\r\n")
if err != nil {
log.Error("refresh Assets Nodes err:", err)
}
}
func (s *sshInteractive) loadUserAssets() {
assets, err := appService.GetUserAssets(s.user.Id)
if err != nil {
log.Error("load Assets failed")
return
}
log.Info("load Assets success")
Cached.Store(s.user.Id, assets)
s.assetData.Store(AssetsMapKey, assets)
}
func (s *sshInteractive) loadUserAssetNodes() {
assetNodes, err := appService.GetUserAssetNodes(s.user.Id)
if err != nil {
log.Error("load Asset Nodes failed")
return
}
log.Info("load Asset Nodes success")
s.assetData.Store(AssetNodesMapKey, assetNodes)
}
func (s *sshInteractive) changeLanguage() {
}
func (s *sshInteractive) JoinShareRoom(roomID string) {
sshConn := &SSHConn{
conn: s.sess,
uuid: generateNewUUID(),
}
ctx, cancelFuc := context.WithCancel(s.sess.Context())
roomID := strings.TrimSpace(strings.Join(strings.Split(searchWord, "join"), ""))
sshConn := &SSHConn{
conn: sess,
uuid: generateNewUUID(),
_, winCh, _ := s.sess.Pty()
go func() {
for {
select {
case <-ctx.Done():
return
case win, ok := <-winCh:
if !ok {
return
}
fmt.Println("join term change:", win)
}
}
log.Info("join room id: ", roomID)
ctx, cancelFuc := context.WithCancel(context.Background())
}()
core.Manager.JoinShareRoom(roomID, sshConn)
log.Info("exit room id:", roomID)
cancelFuc()
}
_, winCh, _ := sess.Pty()
func (s *sshInteractive) StartDispatch() {
_, winCh, _ := s.sess.Pty()
for {
ctx, cancelFuc := context.WithCancel(s.sess.Context())
go func() {
for {
select {
case <-ctx.Done():
log.Info("ctx done")
return
case win, ok := <-winCh:
if !ok {
return
}
fmt.Println("join term change:", win)
log.Info("InteractiveHandler term change:", win)
_ = s.term.SetSize(win.Width, win.Height)
}
}
}()
core.JoinShareRoom(roomID, sshConn)
log.Info("exit room id:", roomID)
line, err := s.term.ReadLine()
cancelFuc()
return
}
if node, ok := searchNode(searchWord); ok {
err := MyProxy(sess, node)
if err != nil {
log.Info("proxy err ", err)
log.Error("ReadLine done", err)
break
}
if line == "" {
continue
}
s.onceLoad.Do(func() {
if _, ok := Cached.Load(s.user.Id); !ok {
s.loadUserAssets()
s.loadUserAssetNodes()
} else {
log.Info("first load this user asset data ")
go func() {
s.loadUserAssets()
s.loadUserAssetNodes()
}()
}
})
if len(line) == 1 {
switch line {
case "p", "P":
if assets, ok := s.assetData.Load(AssetsMapKey); ok {
s.displayAssets(assets.([]model.Asset))
s.currentSearchAssets = assets.([]model.Asset)
} else if assets, _ := Cached.Load(s.user.Id); ok {
s.displayAssets(assets.([]model.Asset))
s.currentSearchAssets = assets.([]model.Asset)
}
case "g", "G":
if assetNodes, ok := s.assetData.Load(AssetNodesMapKey); ok {
s.displayAssetNodes(assetNodes.([]model.AssetNode))
} else {
s.displayAssetNodes([]model.AssetNode{})
}
case "s", "S":
s.changeLanguage()
case "h", "H":
s.displayHelpInfo()
case "r", "R":
s.refreshAssetsAndNodesData()
case "q", "Q":
log.Info("exit session")
return
default:
assets := s.searchAsset(line)
s.currentSearchAssets = assets
s.displayAssetsOrProxy(assets)
}
continue
}
if strings.Index(line, "/") == 0 {
searchWord := strings.TrimSpace(strings.TrimPrefix(line, "/"))
assets := s.searchAsset(searchWord)
s.currentSearchAssets = assets
s.displayAssets(assets)
continue
}
if strings.Index(line, "g") == 0 {
searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g"))
if num, err := strconv.Atoi(searchWord); err == nil {
if num >= 0 {
assets := s.searchNodeAssets(num)
s.displayAssets(assets)
s.currentSearchAssets = assets
continue
}
}
}
if strings.Index(line, "join") == 0 {
roomID := strings.TrimSpace(strings.TrimPrefix(line, "join"))
s.JoinShareRoom(roomID)
continue
}
assets := s.searchAsset(line)
s.currentSearchAssets = assets
s.displayAssetsOrProxy(assets)
}
}
func searchNode(key string) (asset.Node, bool) {
if key == "docker" {
return asset.Node{
IP: "127.0.0.1",
Port: "32768",
UserName: "root",
PassWord: "screencast",
}, true
func (s *sshInteractive) searchAsset(key string) (assets []model.Asset) {
if indexNum, err := strconv.Atoi(key); err == nil {
if indexNum > 0 && indexNum <= len(s.currentSearchAssets) {
return []model.Asset{s.currentSearchAssets[indexNum-1]}
}
}
return asset.Node{}, false
if assetsData, ok := s.assetData.Load(AssetsMapKey); ok {
for _, assetValue := range assetsData.([]model.Asset) {
if isSubstring([]string{assetValue.Ip, assetValue.Hostname, assetValue.Comment}, key) {
assets = append(assets, assetValue)
}
}
} else {
assetsData, _ := Cached.Load(s.user.Id)
for _, assetValue := range assetsData.([]model.Asset) {
if isSubstring([]string{assetValue.Ip, assetValue.Hostname, assetValue.Comment}, key) {
assets = append(assets, assetValue)
}
}
}
return assets
}
func MyProxy(userSess ssh.Session, node asset.Node) error {
func (s *sshInteractive) searchNodeAssets(num int) (assets []model.Asset) {
var assetNodesData []model.AssetNode
if assetNodes, ok := s.assetData.Load(AssetNodesMapKey); ok {
assetNodesData = assetNodes.([]model.AssetNode)
if num > len(assetNodesData) || num == 0 {
return assets
}
return assetNodesData[num-1].AssetsGranted
}
return assets
}
func (s *sshInteractive) Proxy(asset model.Asset, systemUser model.SystemUserAuthInfo) error {
/*
1. 创建SSHConn,符合core.Conn接口
2. 创建一个session Home
......@@ -185,46 +360,65 @@ func MyProxy(userSess ssh.Session, node asset.Node) error {
4. session Home 与 proxy channel 交换数据
*/
sshConn := &SSHConn{
conn: userSess,
conn: s.sess,
uuid: generateNewUUID(),
}
serverAuth := core.ServerAuth{
IP: asset.Ip,
Port: asset.Port,
UserName: systemUser.UserName,
Password: systemUser.Password,
PublicKey: parsePrivateKey(systemUser.PrivateKey)}
userHome := core.NewUserSessionHome(sshConn)
log.Info("session room id:", userHome.SessionID())
c, s, err := CreateNodeSession(node)
nodeConn, err := core.NewNodeConn(serverAuth, sshConn)
if err != nil {
log.Error(err)
return err
}
nodeC, err := core.NewNodeConn(c, s, sshConn)
defer nodeConn.Close()
memChan := core.NewMemoryChannel(nodeConn, sshConn)
userHome := core.NewUserSessionHome(sshConn)
log.Info("session Home ID: ", userHome.SessionID())
err = core.Manager.Switch(context.TODO(), userHome, memChan)
if err != nil {
return err
log.Error(err)
}
mc := core.NewMemoryChannel(nodeC)
err = core.Switch(sshConn.Context(), userHome, mc)
return err
}
func CreateNodeSession(node asset.Node) (c *gossh.Client, s *gossh.Session, err error) {
config := &gossh.ClientConfig{
User: node.UserName,
Auth: []gossh.AuthMethod{
gossh.Password(node.PassWord),
gossh.PublicKeys(node.PublicKey),
},
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}
client, err := gossh.Dial("tcp", node.IP+":"+node.Port, config)
if err != nil {
log.Info(err)
return c, s, err
}
s, err = client.NewSession()
if err != nil {
log.Error(err)
return c, s, err
func isSubstring(sArray []string, substr string) bool {
for _, s := range sArray {
if strings.Contains(s, substr) {
return true
}
}
return false
}
return client, s, nil
func ConstructAssetNodeTree(assetNodes []model.AssetNode) treeprint.Tree {
model.SortAssetNodesByKey(assetNodes)
var treeMap = map[string]treeprint.Tree{}
tree := treeprint.New()
for i := 0; i < len(assetNodes); i++ {
r := strings.LastIndex(assetNodes[i].Key, ":")
if r < 0 {
subtree := tree.AddBranch(fmt.Sprintf("%s.%s(%s)",
strconv.Itoa(i+1), assetNodes[i].Name,
strconv.Itoa(assetNodes[i].AssetsAmount)))
treeMap[assetNodes[i].Key] = subtree
continue
}
if subtree, ok := treeMap[assetNodes[i].Key[:r]]; ok {
nodeTree := subtree.AddBranch(fmt.Sprintf("%s.%s(%s)",
strconv.Itoa(i+1), assetNodes[i].Name,
strconv.Itoa(assetNodes[i].AssetsAmount)))
treeMap[assetNodes[i].Key] = nodeTree
}
}
return tree
}
package sshd
type CommandData struct {
Input string `json:"input"`
Output string `json:"output"`
Timestamp int64 `json:"timestamp"`
}
......@@ -2,41 +2,85 @@ package sshd
import (
"cocogo/pkg/auth"
"cocogo/pkg/config"
"cocogo/pkg/model"
"io"
"strconv"
"sync"
"text/template"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
"github.com/gliderlabs/ssh"
"github.com/sirupsen/logrus"
)
var (
SSHPort int
SSHKeyPath string
log *logrus.Logger
displayTemplate *template.Template
authService *auth.Service
sessionContainer sync.Map
conf *config.Config
appService *auth.Service
serverSig ssh.Signer
displayTemplate *template.Template
log *logrus.Logger
Cached sync.Map
)
func init() {
log = logrus.New()
func Initial(config *config.Config, service *auth.Service) {
displayTemplate = template.Must(template.New("display").Parse(welcomeTemplate))
SSHPort = 2333
SSHKeyPath = "data/host_rsa_key"
authService = auth.NewService()
conf = config
appService = service
serverSig = parsePrivateKey(config.TermConfig.HostKey)
log = logrus.New()
if level, err := logrus.ParseLevel(config.LogLevel); err != nil {
log.SetLevel(logrus.InfoLevel)
} else {
log.SetLevel(level)
}
}
func StartServer() {
serverSig := getPrivateKey(SSHKeyPath)
ser := ssh.Server{
Addr: "0.0.0.0:" + strconv.Itoa(SSHPort),
PasswordHandler: authService.SSHPassword,
HostSigners: []ssh.Signer{serverSig},
Version: "coco-v1.4",
Handler: InteractiveHandler,
Addr: conf.BindHost + ":" + strconv.Itoa(conf.SshPort),
PasswordHandler: appService.CheckSSHPassword,
PublicKeyHandler: appService.CheckSSHPublicKey,
HostSigners: []ssh.Signer{serverSig},
Version: "coco-v1.4",
Handler: connectHandler,
}
log.Fatal(ser.ListenAndServe())
}
func connectHandler(sess ssh.Session) {
_, _, ptyOk := sess.Pty()
if ptyOk {
user, ok := sess.Context().Value("LoginUser").(model.User)
if !ok {
log.Info("Get current User failed")
return
}
userInteractive := &sshInteractive{
sess: sess,
term: terminal.NewTerminal(sess, "Opt>"),
user: user,
helpInfo: HelpInfo{UserName: sess.User(),
ColorCode: GreenColorCode,
ColorEnd: ColorEnd,
Tab: Tab,
EndLine: EndLine}}
log.Info("accept one session")
userInteractive.displayHelpInfo()
userInteractive.StartDispatch()
} else {
_, err := io.WriteString(sess, "No PTY requested.\n")
if err != nil {
return
}
}
}
......@@ -20,6 +20,7 @@ func (s *SSHConn) SessionID() string {
func (s *SSHConn) User() string {
return s.conn.User()
}
func (s *SSHConn) UUID() uuid.UUID {
return s.uuid
}
......
package sshd
import (
"io/ioutil"
uuid "github.com/satori/go.uuid"
gossh "golang.org/x/crypto/ssh"
)
func getPrivateKey(keyPath string) gossh.Signer {
privateBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
log.Fatal("Failed to load private key: ", err)
}
private, err := gossh.ParsePrivateKey(privateBytes)
func parsePrivateKey(privateKey string) gossh.Signer {
private, err := gossh.ParsePrivateKey([]byte(privateKey))
if err != nil {
log.Fatal("Failed to parse private key: ", err)
log.Info("Failed to parse private key: ", err)
}
return private
}
......
package sshd
const welcomeTemplate = `
{{.UserName}} Welcome to use Jumpserver open source fortress system{{.EndLine}}
{{.Tab}}1) Enter {{.ColorCode}}ID{{.ColorEnd}} directly login or enter {{.ColorCode}}part IP, Hostname, Comment{{.ColorEnd}} to search login(if unique). {{.EndLine}}
{{.Tab}}2) Enter {{.ColorCode}}/{{.ColorEnd}} + {{.ColorCode}}IP, Hostname{{.ColorEnd}} or {{.ColorCode}}Comment{{.ColorEnd}} search, such as: /ip. {{.EndLine}}
{{.Tab}}3) Enter {{.ColorCode}}p{{.ColorEnd}} to display the host you have permission.{{.EndLine}}
{{.Tab}}4) Enter {{.ColorCode}}g{{.ColorEnd}} to display the node that you have permission.{{.EndLine}}
{{.Tab}}5) Enter {{.ColorCode}}g{{.ColorEnd}} + {{.ColorCode}}NodeID{{.ColorEnd}} to display the host under the node, such as g1. {{.EndLine}}
{{.Tab}}6) Enter {{.ColorCode}}s{{.ColorEnd}} Chinese-english switch.{{.EndLine}}
{{.Tab}}7) Enter {{.ColorCode}}h{{.ColorEnd}} help.{{.EndLine}}
{{.Tab}}8) Enter {{.ColorCode}}r{{.ColorEnd}} to refresh your assets and nodes.{{.EndLine}}
{{.Tab}}0) Enter {{.ColorCode}}q{{.ColorEnd}} exit.{{.EndLine}}
`
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment