1
0
mirror of https://shylinux.com/x/icebergs synced 2025-05-02 19:47:02 +08:00
This commit is contained in:
shaoying 2020-09-28 17:09:08 +08:00
parent 381e3f3a24
commit c01b3063da
13 changed files with 707 additions and 547 deletions

8
base/mdb/mdb.shy Normal file
View File

@ -0,0 +1,8 @@
chapter "mdb"
field "搜索" mdb.search
field "引擎" mdb.engine
field "插件" mdb.plugin
field "渲染" mdb.render

5
base/nfs/nfs.shy Normal file
View File

@ -0,0 +1,5 @@
chapter "nfs"
field "目录" nfs.dir
field "文件" nfs.file

94
base/ssh/connect.go Normal file
View File

@ -0,0 +1,94 @@
package ssh
import (
ice "github.com/shylinux/icebergs"
"github.com/shylinux/icebergs/base/aaa"
"github.com/shylinux/icebergs/base/mdb"
"github.com/shylinux/icebergs/base/nfs"
"github.com/shylinux/icebergs/base/tcp"
kit "github.com/shylinux/toolkits"
"net"
"os"
"path"
"golang.org/x/crypto/ssh"
)
func _ssh_conn(m *ice.Message, conn net.Conn, username, host, port string) (*ssh.Client, error) {
methods := []ssh.AuthMethod{}
if key, e := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, path.Join(os.Getenv("HOME"), m.Conf(PUBLIC, "meta.private"))))); !m.Warn(e != nil) {
methods = append(methods, ssh.PublicKeys(key))
} else {
return nil, e
}
c, chans, reqs, err := ssh.NewClientConn(conn, host+":"+port, &ssh.ClientConfig{User: username, Auth: methods,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
m.Logs(CONNECT, "hostname", hostname, aaa.HOSTPORT, remote.String())
return nil
},
})
if err != nil {
return nil, err
}
return ssh.NewClient(c, chans, reqs), nil
}
const CONNECT = "connect"
func init() {
Index.Merge(&ice.Context{
Configs: map[string]*ice.Config{
CONNECT: {Name: CONNECT, Help: "连接", Value: kit.Data()},
},
Commands: map[string]*ice.Command{
CONNECT: {Name: "connect hash auto 添加 清理", Help: "连接", Action: map[string]*ice.Action{
tcp.DIAL: {Name: "dial username=shy host=shylinux.com port=22", Help: "添加", Hand: func(m *ice.Message, arg ...string) {
m.Option(tcp.DIAL_CB, func(c net.Conn) {
client, e := _ssh_conn(m, c, kit.Select("shy", m.Option(aaa.USERNAME)), kit.Select(m.Option(tcp.HOST), "shylinux.com"), kit.Select("22", m.Option(tcp.PORT)))
m.Assert(e)
h := m.Rich(CONNECT, "", kit.Dict(
aaa.USERNAME, m.Option(aaa.USERNAME),
tcp.HOST, m.Option(tcp.HOST),
tcp.PORT, m.Option(tcp.PORT),
kit.MDB_STATUS, tcp.OPEN,
CONNECT, client,
))
m.Cmd(CONNECT, SESSION, kit.MDB_HASH, h)
})
m.Cmds(tcp.CLIENT, tcp.DIAL, arg)
}},
SESSION: {Name: "session hash", Help: "会话", Hand: func(m *ice.Message, arg ...string) {
m.Richs(CONNECT, "", m.Option(kit.MDB_HASH), func(key string, value map[string]interface{}) {
client, ok := value[CONNECT].(*ssh.Client)
if !ok {
return
}
h := m.Rich(SESSION, "", kit.Data(
kit.MDB_STATUS, tcp.OPEN,
CONNECT, key,
))
if session, e := _ssh_sess(m, h, client); m.Assert(e) {
session.Shell()
session.Wait()
}
})
}},
mdb.PRUNES: {Name: "prunes", Help: "清理", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.PRUNES, CONNECT, "", mdb.HASH, kit.MDB_STATUS, tcp.CLOSE)
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Option(mdb.FIELDS, "time,hash,status,username,host,port")
m.Cmdy(mdb.SELECT, CONNECT, "", mdb.HASH, kit.MDB_HASH, arg)
m.PushAction("会话")
}},
},
}, nil)
}

357
base/ssh/script.go Normal file
View File

@ -0,0 +1,357 @@
package ssh
import (
ice "github.com/shylinux/icebergs"
"github.com/shylinux/icebergs/base/aaa"
"github.com/shylinux/icebergs/base/cli"
"github.com/shylinux/icebergs/base/mdb"
kit "github.com/shylinux/toolkits"
"bufio"
"bytes"
"fmt"
"io"
"os"
"path"
"strings"
"time"
)
func Render(msg *ice.Message, cmd string, args ...interface{}) {
defer func() { msg.Log_EXPORT(mdb.RENDER, cmd, kit.MDB_TEXT, args) }()
switch arg := kit.Simple(args...); cmd {
case ice.RENDER_VOID:
case ice.RENDER_RESULT:
fmt.Fprintf(msg.O, msg.Result())
case ice.RENDER_QRCODE:
fmt.Fprintf(msg.O, msg.Cmdx(cli.PYTHON, "qrcode", kit.Format(args[0], args[1:]...)))
case ice.RENDER_DOWNLOAD:
if f, e := os.Open(arg[0]); e == nil {
defer f.Close()
io.Copy(msg.O, f)
}
default:
// 转换结果
res := msg.Result()
if res == "" {
res = msg.Table().Result()
}
args = append(args, "length:", len(res))
// 输出结果
if fmt.Fprintf(msg.O, res); !strings.HasSuffix(res, "\n") {
fmt.Fprintf(msg.O, "\n")
}
}
}
func Script(m *ice.Message, name string) io.Reader {
if b, ok := ice.BinPack[name]; ok {
m.Debug("binpack %v %v", len(b), name)
return bytes.NewReader(b)
}
if strings.Contains(m.Option("_script"), "/") {
name = path.Join(path.Dir(m.Option("_script")), name)
}
m.Option("_script", name)
if s, e := os.Open(name); e == nil {
return s
}
switch strings.Split(name, "/")[0] {
case "etc", "var":
m.Warn(true, ice.ErrNotFound)
return nil
}
if msg := m.Cmd("web.spide", "dev", "GET", path.Join("/share/local/", name)); msg.Result(0) != ice.ErrWarn {
bio := bytes.NewBuffer([]byte(msg.Result()))
return bio
}
if strings.HasPrefix(name, "usr") {
ls := strings.Split(name, "/")
m.Cmd("web.code.git.repos", ls[1], "usr/"+ls[1])
if s, e := os.Open(name); e == nil {
return s
}
}
return nil
}
type Frame struct {
source string
target *ice.Context
stdout io.Writer
count int
ps1 []string
ps2 []string
exit bool
}
func (f *Frame) prompt(m *ice.Message, list ...string) *Frame {
if f.source != STDIO {
return f
}
if len(list) == 0 {
list = append(list, f.ps1...)
}
fmt.Fprintf(f.stdout, "\r")
for _, v := range list {
switch v {
case "count":
fmt.Fprintf(f.stdout, "%d", f.count+1)
case "time":
fmt.Fprintf(f.stdout, time.Now().Format("15:04:05"))
case "target":
fmt.Fprintf(f.stdout, f.target.Name)
default:
fmt.Fprintf(f.stdout, v)
}
}
return f
}
func (f *Frame) printf(m *ice.Message, res string, arg ...interface{}) *Frame {
if len(arg) > 0 {
fmt.Fprintf(f.stdout, res, arg...)
} else {
fmt.Fprint(f.stdout, res)
}
return f
}
func (f *Frame) option(m *ice.Message, ls []string) []string {
ln := []string{}
m.Option("cache.limit", 10)
for i := 0; i < len(ls); i++ {
if ls[i] == "--" {
ln = append(ln, ls[i+1:]...)
break
}
if strings.HasPrefix(ls[i], "-") {
for j := i; j < len(ls); j++ {
if j == len(ls)-1 || strings.HasPrefix(ls[j+1], "-") {
if i < j {
m.Option(ls[i][1:], ls[i+1:j+1])
} else {
m.Option(ls[i][1:], "true")
}
i = j
break
}
}
} else {
ln = append(ln, ls[i])
}
}
return ln
}
func (f *Frame) change(m *ice.Message, ls []string) []string {
if len(ls) == 1 && ls[0] == "~" {
// 模块列表
ls = []string{"context"}
} else if len(ls) > 0 && strings.HasPrefix(ls[0], "~") {
// 切换模块
target := ls[0][1:]
if ls = ls[1:]; len(target) == 0 && len(ls) > 0 {
target, ls = ls[0], ls[1:]
}
if target == "~" {
target = ""
}
m.Spawn(f.target).Search(target+".", func(p *ice.Context, s *ice.Context, key string) {
m.Info("choice: %s", s.Name)
f.target = s
})
}
return ls
}
func (f *Frame) alias(m *ice.Message, ls []string) []string {
if alias, ok := m.Optionv(ice.MSG_ALIAS).(map[string]interface{}); ok {
if len(ls) > 0 {
if a := kit.Simple(alias[ls[0]]); len(a) > 0 {
ls = append(append([]string{}, a...), ls[1:]...)
}
}
}
return ls
}
func (f *Frame) parse(m *ice.Message, line string) string {
if strings.HasPrefix(line, "<") {
fmt.Fprintf(m.O, line)
return ""
}
for _, one := range kit.Split(line, ";", ";", ";") {
m.Log_IMPORT("stdin", one, "length", len(one))
async, one := false, strings.TrimSpace(one)
if strings.TrimSuffix(one, "&") != one {
async, one = true, strings.TrimSuffix(one, "&")
}
msg := m.Spawns(f.target)
msg.Option("_cmd", one)
ls := kit.Split(one)
ls = f.alias(msg, ls)
ls = f.change(msg, ls)
ls = f.option(msg, ls)
if len(ls) == 0 {
continue
}
if async {
msg.Gos(msg, func(msg *ice.Message) { msg.Cmd(ls[0], ls[1:]) })
continue
} else {
msg.Cmdy(ls[0], ls[1:])
}
if strings.HasPrefix(msg.Result(), ice.ErrWarn) && m.Option("render") == "raw" {
fmt.Fprintf(msg.O, line)
continue
}
// 渲染引擎
_args, _ := msg.Optionv(ice.MSG_ARGS).([]interface{})
Render(msg, msg.Option(ice.MSG_OUTPUT), _args...)
}
return ""
}
func (f *Frame) scan(m *ice.Message, line string, r io.Reader) *Frame {
m.Option("ssh.return", func() { f.exit = true })
f.ps1 = kit.Simple(m.Confv("prompt", "meta.PS1"))
f.ps2 = kit.Simple(m.Confv("prompt", "meta.PS2"))
ps := f.ps1
m.I, m.O = r, f.stdout
bio := bufio.NewScanner(r)
for f.prompt(m, ps...); bio.Scan() && !f.exit; f.prompt(m, ps...) {
if len(bio.Text()) == 0 {
continue // 空行
}
if strings.HasSuffix(bio.Text(), "\\") {
line += bio.Text()[:len(bio.Text())-1]
ps = f.ps2
continue // 续行
}
if line += bio.Text(); strings.Count(line, "`")%2 == 1 {
line += "\n"
ps = f.ps2
continue // 多行
}
if strings.HasPrefix(strings.TrimSpace(line), "#") {
line = ""
continue // 注释
}
// if line = f.history(m, line); line == "" {
// // 历史命令
// continue
// }
if ps = f.ps1; f.stdout == os.Stdout {
// 清空格式
f.printf(m, "\033[0m")
}
line = f.parse(m, line)
}
return f
}
func (f *Frame) Begin(m *ice.Message, arg ...string) ice.Server {
return f
}
func (f *Frame) Spawn(m *ice.Message, c *ice.Context, arg ...string) ice.Server {
return &Frame{}
}
func (f *Frame) Start(m *ice.Message, arg ...string) bool {
f.source, f.target = kit.Select(STDIO, arg, 0), m.Target()
var r io.Reader
switch m.Cap(ice.CTX_STREAM, f.source) {
case STDIO: // 终端交互
r, f.stdout = os.Stdin, os.Stdout
m.Option("_option", ice.MSG_USERNAME)
m.Option(ice.MSG_USERNAME, cli.UserName)
m.Option(ice.MSG_USERROLE, aaa.ROOT)
m.Option(ice.MSG_USERZONE, "boot")
aaa.UserRoot(m)
default:
f.target = m.Source()
if strings.HasPrefix(f.source, "/dev") {
r, f.stdout = m.I, m.O
break
}
buf := bytes.NewBuffer(make([]byte, 0, 4096))
defer func() { m.Echo(buf.String()) }()
if s := Script(m, f.source); s != nil {
r, f.stdout = s, buf
break
}
return true
}
f.scan(m, "", r)
return true
}
func (f *Frame) Close(m *ice.Message, arg ...string) bool {
return true
}
const (
STDIO = "stdio"
)
const (
SOURCE = "source"
TARGET = "target"
PROMPT = "prompt"
RETURN = "return"
)
func init() {
Index.Merge(&ice.Context{
Configs: map[string]*ice.Config{
SOURCE: {Name: SOURCE, Help: "加载脚本", Value: kit.Data()},
PROMPT: {Name: PROMPT, Help: "命令提示", Value: kit.Data(
"PS1", []interface{}{"\033[33;44m", "count", "[", "time", "]", "\033[5m", "target", "\033[0m", "\033[44m", ">", "\033[0m ", "\033[?25h", "\033[32m"},
"PS2", []interface{}{"count", " ", "target", "> "},
)},
},
Commands: map[string]*ice.Command{
SOURCE: {Name: "source file", Help: "脚本解析", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Starts(strings.Replace(arg[0], ".", "_", -1), arg[0], arg[0:]...)
}},
TARGET: {Name: "target name", Help: "当前模块", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Search(arg[0], func(p *ice.Context, s *ice.Context, key string) {
f := m.Target().Server().(*Frame)
f.target = s
f.prompt(m)
})
}},
PROMPT: {Name: "prompt arg...", Help: "命令提示", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
f := m.Target().Server().(*Frame)
f.ps1 = arg
f.prompt(m)
}},
RETURN: {Name: "return", Help: "结束脚本", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
switch cb := m.Optionv("ssh.return").(type) {
case func():
cb()
}
}},
},
}, nil)
}

View File

@ -51,43 +51,6 @@ func _ssh_close(m *ice.Message, c net.Conn, channel ssh.Channel) {
defer channel.Close()
channel.Write([]byte(m.Conf(PUBLIC, "meta.goodbye")))
}
func _ssh_trace(m *ice.Message, meta map[string]string, input io.Reader, output io.Writer, display io.Writer) {
m.Gos(m, func(m *ice.Message) {
i, buf := 0, make([]byte, 1024)
for {
n, e := input.Read(buf[i:])
if e != nil {
break
}
switch buf[i] {
case '\r', '\n':
cmd := strings.TrimSpace(string(buf[:i]))
m.Log_IMPORT("hostname", meta["hostname"], "username", meta["username"], "buf", buf[:i+n])
m.Conf(CONNECT, kit.Keys(kit.MDB_HASH, meta[CONNECT], "duration"), m.Format("cost"))
m.Conf(SESSION, kit.Keys(kit.MDB_HASH, meta[SESSION], "cmd"), cmd)
msg := m.Cmd(cmd).Table()
res := strings.TrimSpace(strings.ReplaceAll(msg.Result(), "\n", "\r\n"))
if len(res) > 0 {
fmt.Fprintf(display, "\r\n")
fmt.Fprintf(display, res)
fmt.Fprintf(display, "\r\n")
output.Write([]byte{21, 10})
} else {
output.Write(buf[i : i+n])
}
i = 0
default:
output.Write(buf[i : i+n])
if i += n; i >= 1024 {
i = 0
}
}
}
})
}
func _ssh_watch(m *ice.Message, meta map[string]string, input io.Reader, output io.Writer, display io.Writer) {
m.Gos(m, func(m *ice.Message) {
r, w := io.Pipe()
@ -125,13 +88,13 @@ func _ssh_handle(m *ice.Message, meta map[string]string, c net.Conn, channel ssh
shell := kit.Select("bash", os.Getenv("SHELL"))
list := []string{"PATH=" + os.Getenv("PATH")}
tty, f, err := pty.Open()
pty, tty, err := pty.Open()
if m.Warn(err != nil, err) {
return
}
defer f.Close()
defer tty.Close()
h := m.Cmdx(mdb.INSERT, m.Prefix(SESSION), "", mdb.HASH, aaa.HOSTPORT, c.RemoteAddr().String(), kit.MDB_STATUS, "open", "tty", tty.Name())
h := m.Cmdx(mdb.INSERT, m.Prefix(SESSION), "", mdb.HASH, aaa.HOSTPORT, c.RemoteAddr().String(), kit.MDB_STATUS, "open", "pty", pty.Name())
m.Richs(SESSION, "", h, func(key string, value map[string]interface{}) { value["channel"] = channel })
meta[SESSION] = h
@ -142,11 +105,11 @@ func _ssh_handle(m *ice.Message, meta map[string]string, c net.Conn, channel ssh
case "pty-req":
termLen := request.Payload[3]
termEnv := string(request.Payload[4 : termLen+4])
_ssh_size(tty.Fd(), request.Payload[termLen+4:])
_ssh_size(pty.Fd(), request.Payload[termLen+4:])
list = append(list, "TERM="+termEnv)
case "window-change":
_ssh_size(tty.Fd(), request.Payload)
_ssh_size(pty.Fd(), request.Payload)
case "env":
var env struct {
@ -163,47 +126,25 @@ func _ssh_handle(m *ice.Message, meta map[string]string, c net.Conn, channel ssh
channel, func() { channel.Close() })
case "shell":
if meta["username"] == "ssh" {
m.I, m.O = f, f
m.I, m.O = tty, tty
m.Render(ice.RENDER_VOID)
m.Gos(m, func(m *ice.Message) {
m.Cmdy(SOURCE, tty.Name())
m.Cmdy(SOURCE, pty.Name())
_ssh_close(m, c, channel)
})
} else {
_ssh_exec(m, shell, nil, list, f, func() {
_ssh_exec(m, shell, nil, list, tty, func() {
defer m.Cmd(mdb.MODIFY, m.Prefix(SESSION), "", mdb.HASH, kit.MDB_HASH, h, kit.MDB_STATUS, "close")
_ssh_close(m, c, channel)
})
}
m.Gos(m, func(m *ice.Message) { io.Copy(channel, tty) })
_ssh_watch(m, meta, channel, tty, channel)
// _ssh_trace(m, meta, channel, tty, channel)
m.Gos(m, func(m *ice.Message) { io.Copy(channel, pty) })
_ssh_watch(m, meta, channel, pty, channel)
}
request.Reply(true, nil)
}
}
func _ssh_accept(m *ice.Message, c net.Conn) {
sc, sessions, req, err := ssh.NewServerConn(c, _ssh_config(m))
if m.Warn(err != nil, err) {
return
}
m.Gos(m, func(m *ice.Message) { ssh.DiscardRequests(req) })
for session := range sessions {
channel, requests, err := session.Accept()
if m.Warn(err != nil, err) {
continue
}
func(channel ssh.Channel, requests <-chan *ssh.Request) {
m.Gos(m, func(m *ice.Message) {
_ssh_handle(m, sc.Permissions.Extensions, c, channel, requests)
})
}(channel, requests)
}
}
func _ssh_config(m *ice.Message) *ssh.ServerConfig {
config := &ssh.ServerConfig{
BannerCallback: func(conn ssh.ConnMetadata) string {
@ -250,28 +191,10 @@ func _ssh_config(m *ice.Message) *ssh.ServerConfig {
}
return config
}
func _ssh_dial(m *ice.Message, username, hostport string) (*ssh.Client, error) {
methods := []ssh.AuthMethod{}
if key, e := ssh.ParsePrivateKey([]byte(m.Cmdx(nfs.CAT, path.Join(os.Getenv("HOME"), m.Conf(PUBLIC, "meta.private"))))); !m.Warn(e != nil) {
methods = append(methods, ssh.PublicKeys(key))
} else {
return nil, e
}
connect, e := ssh.Dial("tcp", hostport, &ssh.ClientConfig{User: username, Auth: methods,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
m.Logs(CONNECT, "hostname", hostname, aaa.HOSTPORT, remote.String())
return nil
},
})
return connect, e
}
const (
ADDRESS = "address"
CONNECT = "connect"
CHANNEL = "channel"
SESSION = "session"
REQUEST = "request"
COMMAND = "command"
)
@ -295,19 +218,9 @@ func init() {
LISTEN: {Name: LISTEN, Help: "服务", Value: kit.Data(kit.MDB_SHORT, aaa.HOSTPORT,
kit.MDB_FIELD, "time,hash,hostport,status",
)},
CONNECT: {Name: CONNECT, Help: "连接", Value: kit.Data(
kit.MDB_FIELD, "time,hash,hostport,status,duration,close_time,hostname,username",
)},
SESSION: {Name: SESSION, Help: "会话", Value: kit.Data(
kit.MDB_FIELD, "time,hash,hostport,status,tty,cmd",
)},
COMMAND: {Name: COMMAND, Help: "命令", Value: kit.Data(
kit.MDB_FIELD, "time,id,username,hostname,cmd",
)},
DIAL: {Name: DIAL, Help: "连接", Value: kit.Data(
kit.MDB_FIELD, "time,hash,hostport,username",
)},
},
Commands: map[string]*ice.Command{
PUBLIC: {Name: "public hash=auto auto 添加 导出 导入", Help: "公钥", Action: map[string]*ice.Action{
@ -360,26 +273,6 @@ func init() {
}
m.Cmdy(mdb.SELECT, m.Prefix(LISTEN), "", mdb.HASH, kit.MDB_HASH, arg)
}},
CONNECT: {Name: "connect hash=auto auto 清理", Help: "连接", Action: map[string]*ice.Action{
mdb.PRUNES: {Name: "prunes", Help: "清理", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.PRUNES, m.Prefix(CONNECT), "", mdb.HASH, kit.MDB_STATUS, "close")
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if m.Option(mdb.FIELDS, m.Conf(CONNECT, kit.META_FIELD)); len(arg) > 0 {
m.Option(mdb.FIELDS, mdb.DETAIL)
}
m.Cmdy(mdb.SELECT, m.Prefix(CONNECT), "", mdb.HASH, kit.MDB_HASH, arg)
}},
SESSION: {Name: "session hash auto 清理", Help: "会话", Action: map[string]*ice.Action{
mdb.PRUNES: {Name: "prunes", Help: "清理", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.PRUNES, m.Prefix(SESSION), "", mdb.HASH, kit.MDB_STATUS, "close")
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if m.Option(mdb.FIELDS, m.Conf(SESSION, kit.META_FIELD)); len(arg) > 0 {
m.Option(mdb.FIELDS, mdb.DETAIL)
}
m.Cmdy(mdb.SELECT, m.Prefix(SESSION), "", mdb.HASH, kit.MDB_HASH, arg)
}},
COMMAND: {Name: "command id=auto auto", Help: "命令", Action: map[string]*ice.Action{
mdb.PRUNES: {Name: "prunes", Help: "清理", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.PRUNES, m.Prefix(CONNECT), "", mdb.HASH, kit.MDB_STATUS, "close")
@ -390,61 +283,6 @@ func init() {
}
m.Cmdy(mdb.SELECT, m.Prefix(COMMAND), "", mdb.LIST, kit.MDB_ID, arg)
}},
DIAL: {Name: "dial hash=auto auto 添加 导出 导入 cmd:textarea=pwd", Help: "连接", Action: map[string]*ice.Action{
mdb.CREATE: {Name: "create username=shy hostname=shylinux.com port=22", Help: "添加", Hand: func(m *ice.Message, arg ...string) {
if connect, e := _ssh_dial(m, m.Option(aaa.USERNAME), m.Option(aaa.HOSTPORT, m.Option("hostname")+":"+m.Option("port"))); m.Assert(e) {
h := m.Rich(DIAL, "", kit.Dict(aaa.USERNAME, m.Option(aaa.USERNAME), aaa.HOSTPORT, m.Option(aaa.HOSTPORT), CONNECT, connect))
m.Echo(h)
}
}},
mdb.DELETE: {Name: "delete", Help: "删除", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.DELETE, m.Prefix(DIAL), "", mdb.HASH, kit.MDB_HASH, m.Option(kit.MDB_HASH))
}},
mdb.EXPORT: {Name: "export file=.ssh/known_hosts", Help: "导出", Hand: func(m *ice.Message, arg ...string) {
list := []string{}
if m.Cmd(mdb.SELECT, m.Prefix(PUBLIC), "", mdb.HASH).Table(func(index int, value map[string]string, head []string) {
list = append(list, fmt.Sprintf("%s %s %s", value[kit.MDB_TYPE], value[kit.MDB_TEXT], value[kit.MDB_NAME]))
}); len(list) > 0 {
m.Cmdy(nfs.SAVE, path.Join(os.Getenv("HOME"), m.Option(kit.MDB_FILE)), strings.Join(list, "\n")+"\n")
}
}},
mdb.IMPORT: {Name: "import file=.ssh/known_hosts", Help: "导入", Hand: func(m *ice.Message, arg ...string) {
p := path.Join(os.Getenv("HOME"), m.Option(kit.MDB_FILE))
for _, pub := range strings.Split(m.Cmdx(nfs.CAT, p), "\n") {
if len(pub) > 10 {
m.Cmd(PUBLIC, mdb.CREATE, "publickey", pub)
}
}
m.Echo(p)
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if len(arg) == 0 || arg[0] == "" {
m.Option(mdb.FIELDS, m.Conf(DIAL, kit.META_FIELD))
m.Cmdy(mdb.SELECT, m.Prefix(DIAL), "", mdb.HASH)
m.PushAction("删除")
return
}
m.Richs(DIAL, "", arg[0], func(key string, value map[string]interface{}) {
connect, ok := value[CONNECT].(*ssh.Client)
if !ok {
if c, e := _ssh_dial(m, kit.Format(value[aaa.USERNAME]), kit.Format(value[aaa.HOSTPORT])); m.Assert(e) {
connect, value[CONNECT] = c, c
}
}
session, e := connect.NewSession()
m.Assert(e)
defer session.Close()
var b bytes.Buffer
session.Stdout = &b
m.Assert(session.Run(arg[1]))
m.Echo(b.String())
})
}},
},
}, nil)
}

62
base/ssh/service.go Normal file
View File

@ -0,0 +1,62 @@
package ssh
import (
"net"
ice "github.com/shylinux/icebergs"
"github.com/shylinux/icebergs/base/mdb"
"github.com/shylinux/icebergs/base/tcp"
kit "github.com/shylinux/toolkits"
"golang.org/x/crypto/ssh"
)
func _ssh_accept(m *ice.Message, c net.Conn) {
sc, sessions, req, err := ssh.NewServerConn(c, _ssh_config(m))
if m.Warn(err != nil, err) {
return
}
m.Gos(m, func(m *ice.Message) { ssh.DiscardRequests(req) })
for session := range sessions {
channel, requests, err := session.Accept()
if m.Warn(err != nil, err) {
continue
}
func(channel ssh.Channel, requests <-chan *ssh.Request) {
m.Gos(m, func(m *ice.Message) {
_ssh_handle(m, sc.Permissions.Extensions, c, channel, requests)
})
}(channel, requests)
}
}
const SERVICE = "service"
func init() {
Index.Merge(&ice.Context{
Configs: map[string]*ice.Config{
SERVICE: {Name: SERVICE, Help: "服务", Value: kit.Data()},
},
Commands: map[string]*ice.Command{
SERVICE: {Name: "service", Help: "服务", Action: map[string]*ice.Action{
tcp.LISTEN: {Name: "listen name=tcp port=9030", Help: "监听", Hand: func(m *ice.Message, arg ...string) {
m.Option(tcp.LISTEN_CB, func(c net.Conn) {
m.Gos(m.Spawn(), func(msg *ice.Message) { _ssh_accept(msg, c) })
})
m.Gos(m, func(m *ice.Message) {
m.Cmdy(tcp.SERVER, tcp.LISTEN, kit.MDB_NAME, "ssh", tcp.PORT, m.Option(tcp.PORT))
})
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if m.Option(mdb.FIELDS, m.Conf(LISTEN, kit.META_FIELD)); len(arg) > 0 {
m.Option(mdb.FIELDS, mdb.DETAIL)
}
m.Option(mdb.FIELDS, "time,hash,status,host,port")
m.Cmdy(mdb.SELECT, m.Prefix(LISTEN), "", mdb.HASH, kit.MDB_HASH, arg)
}},
},
}, nil)
}

91
base/ssh/session.go Normal file
View File

@ -0,0 +1,91 @@
package ssh
import (
"io"
ice "github.com/shylinux/icebergs"
"github.com/shylinux/icebergs/base/ctx"
"github.com/shylinux/icebergs/base/mdb"
"github.com/shylinux/icebergs/base/tcp"
kit "github.com/shylinux/toolkits"
"golang.org/x/crypto/ssh"
)
func _ssh_sess(m *ice.Message, h string, client *ssh.Client) (*ssh.Session, error) {
session, e := client.NewSession()
m.Assert(e)
out, e := session.StdoutPipe()
m.Assert(e)
in, e := session.StdinPipe()
m.Assert(e)
m.Go(func() {
for {
buf := make([]byte, 1024)
n, e := out.Read(buf)
if e != nil {
break
}
m.Debug(string(buf[:n]))
m.Grow(SESSION, kit.Keys(kit.MDB_HASH, h), kit.Dict(
kit.MDB_TYPE, RES, kit.MDB_TEXT, string(buf[:n]),
))
}
})
m.Richs(SESSION, "", h, func(key string, value map[string]interface{}) {
kit.Value(value, "meta.output", out)
kit.Value(value, "meta.input", in)
})
return session, nil
}
const (
CMD = "cmd"
ARG = "arg"
ENV = "env"
RES = "res"
)
const SESSION = "session"
func init() {
Index.Merge(&ice.Context{
Configs: map[string]*ice.Config{
SESSION: {Name: SESSION, Help: "会话", Value: kit.Data()},
},
Commands: map[string]*ice.Command{
SESSION: {Name: "session hash id auto 命令 清理", Help: "会话", Action: map[string]*ice.Action{
ctx.COMMAND: {Name: "command cmd=pwd", Help: "命令", Hand: func(m *ice.Message, arg ...string) {
m.Richs(SESSION, "", m.Option(kit.MDB_HASH), func(key string, value map[string]interface{}) {
if w, ok := kit.Value(value, "meta.input").(io.Writer); ok {
m.Grow(SESSION, kit.Keys(kit.MDB_HASH, key), kit.Dict(kit.MDB_TYPE, RES, kit.MDB_TEXT, m.Option(CMD)))
n, e := w.Write([]byte(m.Option(CMD) + "\n"))
m.Debug("%v %v", n, e)
}
})
m.Sleep("300ms")
m.Cmdy(SESSION, m.Option(kit.MDB_HASH))
}},
mdb.PRUNES: {Name: "prunes", Help: "清理", Hand: func(m *ice.Message, arg ...string) {
m.Cmdy(mdb.PRUNES, SESSION, "", mdb.HASH, kit.MDB_STATUS, tcp.CLOSE)
}},
}, Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if len(arg) == 0 {
m.Option(mdb.FIELDS, "time,hash,status,count,connect")
m.Cmdy(mdb.SELECT, SESSION, "", mdb.HASH, kit.MDB_HASH, arg)
return
}
m.Option(mdb.FIELDS, "time,id,type,text")
m.Cmdy(mdb.SELECT, SESSION, kit.Keys(kit.MDB_HASH, arg[0]), mdb.LIST, kit.MDB_ID, arg[1:])
m.Sort(kit.MDB_ID)
}},
},
}, nil)
}

View File

@ -2,367 +2,39 @@ package ssh
import (
ice "github.com/shylinux/icebergs"
"github.com/shylinux/icebergs/base/aaa"
"github.com/shylinux/icebergs/base/cli"
"github.com/shylinux/icebergs/base/mdb"
"github.com/shylinux/icebergs/base/tcp"
kit "github.com/shylinux/toolkits"
"bufio"
"bytes"
"fmt"
"io"
"os"
"path"
"strings"
"time"
)
func Render(msg *ice.Message, cmd string, args ...interface{}) {
defer func() { msg.Log_EXPORT(mdb.RENDER, cmd, kit.MDB_TEXT, args) }()
switch arg := kit.Simple(args...); cmd {
case ice.RENDER_VOID:
case ice.RENDER_RESULT:
fmt.Fprintf(msg.O, msg.Result())
case ice.RENDER_QRCODE:
fmt.Fprintf(msg.O, msg.Cmdx(cli.PYTHON, "qrcode", kit.Format(args[0], args[1:]...)))
case ice.RENDER_DOWNLOAD:
if f, e := os.Open(arg[0]); e == nil {
defer f.Close()
io.Copy(msg.O, f)
}
default:
// 转换结果
res := msg.Result()
if res == "" {
res = msg.Table().Result()
}
args = append(args, "length:", len(res))
// 输出结果
if fmt.Fprintf(msg.O, res); !strings.HasSuffix(res, "\n") {
fmt.Fprintf(msg.O, "\n")
}
}
}
func Script(m *ice.Message, name string) io.Reader {
if b, ok := ice.BinPack[name]; ok {
m.Debug("binpack %v %v", len(b), name)
return bytes.NewReader(b)
}
if strings.Contains(m.Option("_script"), "/") {
name = path.Join(path.Dir(m.Option("_script")), name)
}
m.Option("_script", name)
if s, e := os.Open(name); e == nil {
return s
}
switch strings.Split(name, "/")[0] {
case "etc", "var":
m.Warn(true, ice.ErrNotFound)
return nil
}
if msg := m.Cmd("web.spide", "dev", "GET", path.Join("/share/local/", name)); msg.Result(0) != ice.ErrWarn {
bio := bytes.NewBuffer([]byte(msg.Result()))
return bio
}
if strings.HasPrefix(name, "usr") {
ls := strings.Split(name, "/")
m.Cmd("web.code.git.repos", ls[1], "usr/"+ls[1])
if s, e := os.Open(name); e == nil {
return s
}
}
return nil
}
type Frame struct {
source string
target *ice.Context
stdout io.Writer
count int
ps1 []string
ps2 []string
exit bool
}
func (f *Frame) prompt(m *ice.Message, list ...string) *Frame {
if f.source != STDIO {
return f
}
if len(list) == 0 {
list = append(list, f.ps1...)
}
fmt.Fprintf(f.stdout, "\r")
for _, v := range list {
switch v {
case "count":
fmt.Fprintf(f.stdout, "%d", f.count+1)
case "time":
fmt.Fprintf(f.stdout, time.Now().Format("15:04:05"))
case "target":
fmt.Fprintf(f.stdout, f.target.Name)
default:
fmt.Fprintf(f.stdout, v)
}
}
return f
}
func (f *Frame) printf(m *ice.Message, res string, arg ...interface{}) *Frame {
if len(arg) > 0 {
fmt.Fprintf(f.stdout, res, arg...)
} else {
fmt.Fprint(f.stdout, res)
}
return f
}
func (f *Frame) option(m *ice.Message, ls []string) []string {
ln := []string{}
m.Option("cache.limit", 10)
for i := 0; i < len(ls); i++ {
if ls[i] == "--" {
ln = append(ln, ls[i+1:]...)
break
}
if strings.HasPrefix(ls[i], "-") {
for j := i; j < len(ls); j++ {
if j == len(ls)-1 || strings.HasPrefix(ls[j+1], "-") {
if i < j {
m.Option(ls[i][1:], ls[i+1:j+1])
} else {
m.Option(ls[i][1:], "true")
}
i = j
break
}
}
} else {
ln = append(ln, ls[i])
}
}
return ln
}
func (f *Frame) change(m *ice.Message, ls []string) []string {
if len(ls) == 1 && ls[0] == "~" {
// 模块列表
ls = []string{"context"}
} else if len(ls) > 0 && strings.HasPrefix(ls[0], "~") {
// 切换模块
target := ls[0][1:]
if ls = ls[1:]; len(target) == 0 && len(ls) > 0 {
target, ls = ls[0], ls[1:]
}
if target == "~" {
target = ""
}
m.Spawn(f.target).Search(target+".", func(p *ice.Context, s *ice.Context, key string) {
m.Info("choice: %s", s.Name)
f.target = s
})
}
return ls
}
func (f *Frame) alias(m *ice.Message, ls []string) []string {
if alias, ok := m.Optionv(ice.MSG_ALIAS).(map[string]interface{}); ok {
if len(ls) > 0 {
if a := kit.Simple(alias[ls[0]]); len(a) > 0 {
ls = append(append([]string{}, a...), ls[1:]...)
}
}
}
return ls
}
func (f *Frame) parse(m *ice.Message, line string) string {
if strings.HasPrefix(line, "<") {
fmt.Fprintf(m.O, line)
return ""
}
for _, one := range kit.Split(line, ";", ";", ";") {
m.Log_IMPORT("stdin", one, "length", len(one))
async, one := false, strings.TrimSpace(one)
if strings.TrimSuffix(one, "&") != one {
async, one = true, strings.TrimSuffix(one, "&")
}
msg := m.Spawns(f.target)
msg.Option("_cmd", one)
ls := kit.Split(one)
ls = f.alias(msg, ls)
ls = f.change(msg, ls)
ls = f.option(msg, ls)
if len(ls) == 0 {
continue
}
if async {
msg.Gos(msg, func(msg *ice.Message) { msg.Cmd(ls[0], ls[1:]) })
continue
} else {
msg.Cmdy(ls[0], ls[1:])
}
if strings.HasPrefix(msg.Result(), ice.ErrWarn) && m.Option("render") == "raw" {
fmt.Fprintf(msg.O, line)
continue
}
// 渲染引擎
_args, _ := msg.Optionv(ice.MSG_ARGS).([]interface{})
Render(msg, msg.Option(ice.MSG_OUTPUT), _args...)
}
return ""
}
func (f *Frame) scan(m *ice.Message, line string, r io.Reader) *Frame {
m.Option("ssh.return", func() { f.exit = true })
f.ps1 = kit.Simple(m.Confv("prompt", "meta.PS1"))
f.ps2 = kit.Simple(m.Confv("prompt", "meta.PS2"))
ps := f.ps1
m.I, m.O = r, f.stdout
bio := bufio.NewScanner(r)
for f.prompt(m, ps...); bio.Scan() && !f.exit; f.prompt(m, ps...) {
if len(bio.Text()) == 0 {
continue // 空行
}
if strings.HasSuffix(bio.Text(), "\\") {
line += bio.Text()[:len(bio.Text())-1]
ps = f.ps2
continue // 续行
}
if line += bio.Text(); strings.Count(line, "`")%2 == 1 {
line += "\n"
ps = f.ps2
continue // 多行
}
if strings.HasPrefix(strings.TrimSpace(line), "#") {
line = ""
continue // 注释
}
// if line = f.history(m, line); line == "" {
// // 历史命令
// continue
// }
if ps = f.ps1; f.stdout == os.Stdout {
// 清空格式
f.printf(m, "\033[0m")
}
line = f.parse(m, line)
}
return f
}
func (f *Frame) Begin(m *ice.Message, arg ...string) ice.Server {
return f
}
func (f *Frame) Spawn(m *ice.Message, c *ice.Context, arg ...string) ice.Server {
return &Frame{}
}
func (f *Frame) Start(m *ice.Message, arg ...string) bool {
f.source, f.target = kit.Select(STDIO, arg, 0), m.Target()
var r io.Reader
switch m.Cap(ice.CTX_STREAM, f.source) {
case STDIO: // 终端交互
r, f.stdout = os.Stdin, os.Stdout
m.Option("_option", ice.MSG_USERNAME)
m.Option(ice.MSG_USERNAME, cli.UserName)
m.Option(ice.MSG_USERROLE, aaa.ROOT)
m.Option(ice.MSG_USERZONE, "boot")
aaa.UserRoot(m)
default:
f.target = m.Source()
if strings.HasPrefix(f.source, "/dev") {
r, f.stdout = m.I, m.O
break
}
buf := bytes.NewBuffer(make([]byte, 0, 4096))
defer func() { m.Echo(buf.String()) }()
if s := Script(m, f.source); s != nil {
r, f.stdout = s, buf
break
}
return true
}
f.scan(m, "", r)
return true
}
func (f *Frame) Close(m *ice.Message, arg ...string) bool {
return true
}
const (
STDIO = "stdio"
)
const (
SOURCE = "source"
TARGET = "target"
PROMPT = "prompt"
RETURN = "return"
)
const SSH = "ssh"
var Index = &ice.Context{Name: SSH, Help: "终端模块",
Configs: map[string]*ice.Config{
SOURCE: {Name: SOURCE, Help: "加载脚本", Value: kit.Data()},
PROMPT: {Name: PROMPT, Help: "命令提示", Value: kit.Data(
"PS1", []interface{}{"\033[33;44m", "count", "[", "time", "]", "\033[5m", "target", "\033[0m", "\033[44m", ">", "\033[0m ", "\033[?25h", "\033[32m"},
"PS2", []interface{}{"count", " ", "target", "> "},
)},
},
Commands: map[string]*ice.Command{
ice.CTX_INIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) { m.Load() }},
ice.CTX_EXIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if _, ok := m.Target().Server().(*Frame); ok {
m.Done()
var Index = &ice.Context{Name: SSH, Help: "终端模块", Commands: map[string]*ice.Command{
ice.CTX_INIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Load()
}},
ice.CTX_EXIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
if _, ok := m.Target().Server().(*Frame); ok {
m.Done()
}
m.Richs(CONNECT, "", kit.MDB_FOREACH, func(key string, value map[string]interface{}) {
if value[kit.MDB_META] != nil {
value = value[kit.MDB_META].(map[string]interface{})
}
m.Conf(SESSION, kit.MDB_HASH, "")
m.Conf(CONNECT, kit.MDB_HASH, "")
m.Save()
}},
kit.Value(value, "status", tcp.CLOSE)
})
m.Richs(SESSION, "", kit.MDB_FOREACH, func(key string, value map[string]interface{}) {
if value[kit.MDB_META] != nil {
value = value[kit.MDB_META].(map[string]interface{})
}
kit.Value(value, "status", tcp.CLOSE)
})
m.Save()
}},
}}
SOURCE: {Name: "source file", Help: "脚本解析", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Starts(strings.Replace(arg[0], ".", "_", -1), arg[0], arg[0:]...)
}},
TARGET: {Name: "target name", Help: "当前模块", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Search(arg[0], func(p *ice.Context, s *ice.Context, key string) {
f := m.Target().Server().(*Frame)
f.target = s
f.prompt(m)
})
}},
PROMPT: {Name: "prompt arg...", Help: "命令提示", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
f := m.Target().Server().(*Frame)
f.ps1 = arg
f.prompt(m)
}},
RETURN: {Name: "return", Help: "结束脚本", Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
switch cb := m.Optionv("ssh.return").(type) {
case func():
cb()
}
}},
},
func init() {
ice.Index.Register(Index, &Frame{},
SOURCE, TARGET, PROMPT, RETURN,
CONNECT, SESSION, SERVICE,
)
}
func init() { ice.Index.Register(Index, &Frame{}, SOURCE, TARGET, RETURN) }

View File

@ -1,17 +1,17 @@
title "ssh"
chapter "ssh"
refer `
官网 http://www.openssh.com/
源码 https://github.com/openssh/openssh-portable
文档 https://man.openbsd.org/ssh
源码 https://github.com/openssh/openssh-portable
`
chapter "应用"
field "登录" ssh.dial
field "公钥" ssh.public
field "服务" ssh.listen
field "连接" ssh.connect
field "会话" ssh.session
return
chapter "应用"
field "公钥" ssh.public
field "服务" ssh.listen
field "命令" ssh.command

View File

@ -18,10 +18,16 @@ var Index = &ice.Context{Name: TCP, Help: "通信模块",
}},
ice.CTX_EXIT: {Hand: func(m *ice.Message, c *ice.Context, cmd string, arg ...string) {
m.Richs(CLIENT, "", kit.MDB_FOREACH, func(key string, value map[string]interface{}) {
kit.Value(value, "meta.status", CLOSE)
if value[kit.MDB_META] != nil {
value = value[kit.MDB_META].(map[string]interface{})
}
kit.Value(value, "status", CLOSE)
})
m.Richs(SERVER, "", kit.MDB_FOREACH, func(key string, value map[string]interface{}) {
kit.Value(value, "meta.status", CLOSE)
if value[kit.MDB_META] != nil {
value = value[kit.MDB_META].(map[string]interface{})
}
kit.Value(value, "status", CLOSE)
})
m.Save()
}},

View File

@ -1,6 +1,7 @@
chapter "tcp"
field host tcp.host
# field port tcp.port
field server tcp.server
field client tcp.client
field "主机" tcp.host
field "端口" tcp.port
field "服务器" tcp.server
field "客户端" tcp.client

20
exec.go
View File

@ -109,13 +109,29 @@ func (m *Message) Back(res *Message) *Message {
}
return m
}
func (m *Message) Gos(msg *Message, cb func(*Message)) *Message {
func (m *Message) Gos(msg *Message, cb interface{}) *Message {
m.Cmd("gdb.routine", "create", "fileline", kit.FileLine(cb, 3))
task.Put(nil, func(task *task.Task) error {
msg.Optionv("_task", task)
msg.TryCatch(msg, true, func(msg *Message) { cb(msg) })
msg.TryCatch(msg, true, func(msg *Message) {
switch cb := cb.(type) {
case func(*Message):
cb(msg)
case func():
cb()
}
})
return nil
})
return m
}
func (m *Message) Go(cb interface{}) *Message {
switch cb := cb.(type) {
case func(*Message):
return m.Gos(m.Spawn(), cb)
case func():
return m.Gos(m, cb)
}
return m.Gos(m, cb)
}

18
type.go
View File

@ -114,7 +114,11 @@ func (c *Context) cmd(m *Message, cmd *Command, key string, arg ...string) *Mess
}
}
m.Log(LOG_CMDS, "%s.%s %d %v %s", c.Name, key, len(arg), arg, kit.FileLine(cmd.Hand, 3))
if m.target.Name == "mdb" {
m.Log(LOG_CMDS, "%s.%s %d %v %s", c.Name, key, len(arg), arg, kit.FileLine(8, 3))
} else {
m.Log(LOG_CMDS, "%s.%s %d %v %s", c.Name, key, len(arg), arg, kit.FileLine(cmd.Hand, 3))
}
cmd.Hand(m, c, key, arg...)
return m
}
@ -620,12 +624,12 @@ func (m *Message) Search(key interface{}, cb interface{}) *Message {
}
func (m *Message) Cmdy(arg ...interface{}) *Message {
return m.Copy(m.Cmd(arg...))
return m.Copy(m.__cmd(arg...))
}
func (m *Message) Cmdx(arg ...interface{}) string {
return kit.Select("", m.Cmd(arg...).meta[MSG_RESULT], 0)
return kit.Select("", m.__cmd(arg...).meta[MSG_RESULT], 0)
}
func (m *Message) Cmd(arg ...interface{}) *Message {
func (m *Message) __cmd(arg ...interface{}) *Message {
list := kit.Simple(arg...)
if len(list) == 0 && m.Hand == false {
list = m.meta[MSG_DETAIL]
@ -645,6 +649,12 @@ func (m *Message) Cmd(arg ...interface{}) *Message {
}
return m
}
func (m *Message) Cmds(arg ...interface{}) *Message {
return m.Go(func() { m.__cmd(arg...) })
}
func (m *Message) Cmd(arg ...interface{}) *Message {
return m.__cmd(arg...)
}
func (m *Message) Confm(key string, chain interface{}, cbs ...interface{}) map[string]interface{} {
val := m.Confv(key, chain)
if len(cbs) > 0 {