Commit c8e5fb12 authored by Eric's avatar Eric Committed by Eric

init coco go project

parent 3605d72b
......@@ -10,3 +10,7 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
data/
log/
go.sum
.idea/
\ No newline at end of file
package main
import "cocogo/pkg/sshd"
func main() {
sshd.StartServer()
}
module cocogo
go 1.12
require (
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/gdamore/tcell v1.1.1
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/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.4.0
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576
)
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"
type Service struct {
}
var (
service = new(Service)
)
func NewService() *Service {
return service
}
func (s *Service) SSHPassword(ctx ssh.Context, password string) bool {
Username := "softwareuser1"
Password := "123456"
if ctx.User() == Username && password == Password {
return true
}
return false
}
package sshd
import (
"bytes"
"cocogo/pkg/asset"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gliderlabs/ssh"
uuid "github.com/satori/go.uuid"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
const (
maxBufferSize = 1024 * 4
)
type NodeProxy struct {
uuid string
userSess ssh.Session
nodeSess *gossh.Session
nodeClient *gossh.Client
started bool // 记录开始
inputStatus bool // 是否是用户的输入状态
userInputBuf *bytes.Buffer // 用户输入的数据
nodeCmdInputBuf *bytes.Buffer // node对用户输入的回写数据
nodeCmdOutputBuf *bytes.Buffer // node对用户按下enter按键之后,返回的数据
nodeResponseCmdInputBuf []byte
nodeResponseCmdOutputBuf []byte
sendUserStream chan []byte
sendNodeStream chan []byte
sync.Mutex
ctx context.Context
nodeClosed bool
userClosed bool
term *terminal.Terminal
rulerFilters []RuleFilter
specialCommands []SpecialRuler
forbiddenSignal bool
inSpecialStatus bool
}
func NewNodeProxy(nodec *gossh.Client, nodes *gossh.Session, uers ssh.Session) *NodeProxy {
userInputBuf := new(bytes.Buffer)
return &NodeProxy{
uuid: uuid.NewV4().String(),
userSess: uers,
nodeSess: nodes,
nodeClient: nodec,
started: false,
inputStatus: false,
ctx: context.Background(),
nodeClosed: false,
userClosed: false,
userInputBuf: userInputBuf,
nodeCmdInputBuf: new(bytes.Buffer),
nodeCmdOutputBuf: new(bytes.Buffer),
sendUserStream: make(chan []byte),
sendNodeStream: make(chan []byte),
specialCommands: []SpecialRuler{},
term: terminal.NewTerminal(userInputBuf, ""),
}
}
func (n *NodeProxy) receiveNodeResponse(wg *sync.WaitGroup) {
defer wg.Done()
nodeStdout, err := n.nodeSess.StdoutPipe()
if err != nil {
log.Info(err)
return
}
readBuf := make([]byte, maxBufferSize)
for {
nr, err := nodeStdout.Read(readBuf)
if err != nil {
break
}
if nr > 0 {
/*
是否是特殊命令状态:
直接放过;
是否是命令输入:
是:
放入nodeCmdInputBuf
否:
放入nodeCmdOutputBuf
*/
//开始输入之后,才开始记录输入的内容
if n.started {
// 对返回值进行解析,是否进入了特殊命令状态
n.SpecialCommandFilter(readBuf[:nr])
switch {
case n.InSpecialCommandStatus():
// 进入特殊命令状态,
case n.forbiddenSignal:
// 阻断命令的返回值
case n.inputStatus:
n.nodeCmdInputBuf.Write(readBuf[:nr])
default:
n.nodeCmdOutputBuf.Write(readBuf[:nr])
}
}
n.sendUserStream <- readBuf[:nr]
}
}
n.nodeClosed = true
close(n.sendUserStream)
}
func (n *NodeProxy) sendUserResponse(wg *sync.WaitGroup) {
defer wg.Done()
for resBytes := range n.sendUserStream {
nw, err := n.userSess.Write(resBytes)
if nw != len(resBytes) || err != nil {
break
}
}
}
func (n *NodeProxy) receiveUserRequest(wg *sync.WaitGroup) {
defer wg.Done()
readBuf := make([]byte, 1024)
once := sync.Once{}
path := filepath.Join("log", n.uuid)
cmdRecord, _ := os.Create(path)
defer cmdRecord.Close()
var currentCommandInput string
var currentCommandResult string
for {
nr, err := n.userSess.Read(readBuf)
once.Do(func() {
n.started = true
})
if err != nil {
break
}
if n.nodeClosed {
break
}
if nr > 0 {
// 当inputStatus 为false
/*
enter之后
是否需要解析
是:
解析用户真实执行的命令
过滤命令:
1、阻断则发送阻断msg 向node发送清除命令 和换行
否:
直接放过
*/
switch {
case n.InSpecialCommandStatus():
// vim 或者 rz 等状态
case isEnterKey(readBuf[:nr]):
currentCommandInput = n.ParseCommandInput()
if currentCommandInput != "" && n.FilterCommand(currentCommandInput) {
log.Info("cmd forbidden------>", currentCommandInput)
msg := fmt.Sprintf("\r\n cmd '%s' is forbidden \r\n", currentCommandInput)
n.sendUserStream <- []byte(msg)
ctrU := []byte{21, 13} // 清除所有的输入
n.inputStatus = true
n.sendNodeStream <- ctrU
n.forbiddenSignal = true
data := CommandData{
Input: currentCommandInput,
Output: string(msg),
Timestamp: time.Now().UTC().UnixNano(),
}
b, _ := json.Marshal(data)
log.Info("write json data to file.")
cmdRecord.Write(b)
cmdRecord.Write([]byte("\r\n"))
currentCommandInput = ""
currentCommandResult = ""
n.resetNodeInputOutBuf()
continue
}
n.nodeCmdInputBuf.Reset()
n.inputStatus = false
default:
fmt.Println(readBuf[:nr])
if len(n.nodeCmdOutputBuf.Bytes()) > 0 && currentCommandInput != "" {
log.Info("write cmd and result")
currentCommandResult = n.ParseCommandResult()
data := CommandData{
Input: currentCommandInput,
Output: currentCommandResult,
Timestamp: time.Now().UTC().UnixNano(),
}
b, _ := json.Marshal(data)
log.Info("write json data to file.")
cmdRecord.Write(b)
n.resetNodeInputOutBuf()
currentCommandInput = ""
currentCommandResult = ""
}
n.inputStatus = true
n.forbiddenSignal = false
}
//解析命令且过滤命令
n.sendNodeStream <- readBuf[:nr]
}
}
close(n.sendNodeStream)
n.nodeSess.Close()
log.Info("receiveUserRequest exit---->")
}
func (n *NodeProxy) sendNodeRequest(wg *sync.WaitGroup) {
defer wg.Done()
nodeStdin, err := n.nodeSess.StdinPipe()
if err != nil {
return
}
for reqBytes := range n.sendNodeStream {
nw, err := nodeStdin.Write(reqBytes)
if nw != len(reqBytes) || err != nil {
n.nodeClosed = true
break
}
}
log.Info("sendNodeStream closed")
}
// 匹配特殊命令,
func (n *NodeProxy) SpecialCommandFilter(b []byte) {
for _, specialCommand := range n.specialCommands {
if matched := specialCommand.MatchRule(b); matched {
switch {
case specialCommand.EnterStatus():
n.inSpecialStatus = true
case specialCommand.ExitStatus():
n.inSpecialStatus = false
}
}
}
}
func (n *NodeProxy) MatchedSpecialCommand() (SpecialRuler, bool) {
return nil, false
}
func (n *NodeProxy) InSpecialCommandStatus() bool {
return n.inSpecialStatus
}
// 解析命令
func (n *NodeProxy) ParseCommandInput() string {
// 解析用户输入的命令
return n.nodeCmdInputBuf.String()
}
// 解析命令结果
func (n *NodeProxy) ParseCommandResult() string {
return n.nodeCmdOutputBuf.String()
}
// 过滤所有的规则,判断是否阻止命令;如果是空字符串直接返回false
func (n *NodeProxy) FilterCommand(cmd string) bool {
if strings.TrimSpace(cmd) == "" {
return false
}
n.Lock()
defer n.Unlock()
for _, rule := range n.rulerFilters {
if rule.Match(cmd) {
log.Info("match rule", rule)
return rule.BlockCommand()
}
}
return false
}
func (n *NodeProxy) replayFileName() string {
return fmt.Sprintf("%s.replay", n.uuid)
}
// 加载该资产的过滤规则
func (n *NodeProxy) LoadRuleFilters() {
r1 := &Rule{
priority: 10,
action: actionDeny,
contents: []string{"ls"},
ruleType: "command",
}
r2 := &Rule{
priority: 10,
action: actionDeny,
contents: []string{"pwd"},
ruleType: "command",
}
n.Lock()
defer n.Unlock()
n.rulerFilters = []RuleFilter{r1, r2}
}
func (n *NodeProxy) resetNodeInputOutBuf() {
n.nodeCmdInputBuf.Reset()
n.nodeCmdOutputBuf.Reset()
}
func (n *NodeProxy) Start() error {
var (
err error
wg sync.WaitGroup
)
winChangeDone := make(chan struct{})
ptyreq, winCh, _ := n.userSess.Pty()
err = n.nodeSess.RequestPty(ptyreq.Term, ptyreq.Window.Height, ptyreq.Window.Width, gossh.TerminalModes{})
if err != nil {
return err
}
wg.Add(5)
go func() {
defer wg.Done()
for {
select {
case <-winChangeDone:
return
case win := <-winCh:
err = n.nodeSess.WindowChange(win.Height, win.Width)
if err != nil {
return
}
log.Info("windowChange: ", win)
}
}
}()
go n.receiveUserRequest(&wg)
go n.sendNodeRequest(&wg)
go n.receiveNodeResponse(&wg)
go n.sendUserResponse(&wg)
err = n.nodeSess.Shell()
if err != nil {
return err
}
err = n.nodeSess.Wait()
winChangeDone <- struct{}{}
wg.Wait()
log.Info("wg done --->")
if err != nil {
return err
}
return nil
}
func CreateAssetNodeSession(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
}
return client, s, nil
}
func Proxy(userSess ssh.Session, node asset.Node) error {
nodeclient, nodeSess, err := CreateAssetNodeSession(node)
if err != nil {
return err
}
nproxy := NewNodeProxy(nodeclient, nodeSess, userSess)
log.Info("session_uuid_id:", nproxy.uuid)
nproxy.LoadRuleFilters()
err = nproxy.Start()
if err != nil {
log.Error("nproxy err:", err)
return err
}
fmt.Println("exit-------> Proxy")
return nil
}
func isEnterKey(b []byte) bool {
return b[0] == 13
}
package sshd
import (
"cocogo/pkg/asset"
"io"
"strings"
"github.com/gliderlabs/ssh"
"golang.org/x/crypto/ssh/terminal"
)
const Displaytemplate = `
{{.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
ColorEnd string
Tab string
EndLine string
}
func (d HelpInfo) displayHelpInfo(sess ssh.Session) {
e := displayTemplate.Execute(sess, d)
if e != nil {
log.Warn("display help info failed")
}
}
func InteractiveHandler(sess ssh.Session) {
_, _, ptyOk := sess.Pty()
if ptyOk {
helpInfo := HelpInfo{
UserName: sess.User(),
ColorCode: "\033[32m",
ColorEnd: "\033[0m",
Tab: "\t",
EndLine: "\r\n\r",
}
log.Info("accept one session")
helpInfo.displayHelpInfo(sess)
term := terminal.NewTerminal(sess, "Opt>")
for {
line, err := term.ReadLine()
if err != nil {
log.Error(err)
break
}
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)
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)
}
}
} else {
_, err := io.WriteString(sess, "No PTY requested.\n")
if err != nil {
return
}
}
}
func searchNodeAndProxy(line string, sess ssh.Session) {
searchKey := strings.TrimPrefix(line, "/")
if node, ok := searchNode(searchKey); ok {
err := Proxy(sess, node)
if err != nil {
log.Info("proxy err ", err)
}
}
}
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
}
return asset.Node{}, false
}
package sshd
type CommandData struct {
Input string `json:"input"`
Output string `json:"output"`
Timestamp int64 `json:"timestamp"`
}
package sshd
import (
"cocogo/pkg/auth"
"strconv"
"sync"
"text/template"
"github.com/sirupsen/logrus"
"github.com/gliderlabs/ssh"
)
var (
SSHPort int
SSHKeyPath string
log *logrus.Logger
displayTemplate *template.Template
authService *auth.Service
sessionContainer sync.Map
)
func init() {
log = logrus.New()
displayTemplate = template.Must(template.New("display").Parse(Displaytemplate))
SSHPort = 2333
SSHKeyPath = "data/host_rsa_key"
authService = auth.NewService()
}
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,
}
log.Fatal(ser.ListenAndServe())
}
package sshd
type SpecialRuler interface {
// 匹配规则
MatchRule([]byte) bool
// 进入状态
EnterStatus() bool
// 退出状态
ExitStatus() bool
}
package sshd
import (
"io/ioutil"
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)
if err != nil {
log.Fatal("Failed to parse private key: ", err)
}
return private
}
package sshd
import (
"regexp"
)
const (
actionDeny = true
actionAllow = false
)
type RuleFilter interface {
// 判断是否是匹配当前规则
Match(string) bool
// 是否阻断命令
BlockCommand() bool
}
type Rule struct {
priority int
ruleType string
contents []string
action bool
}
func (w *Rule) Match(s string) bool {
switch w.ruleType {
case "command":
for _, content := range w.contents {
if content == s {
return true
}
}
return false
default:
for _, content := range w.contents {
if matched, _ := regexp.MatchString(content, s); matched {
return true
}
}
return false
}
}
func (w *Rule) BlockCommand() bool {
return w.action
}
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