本篇教程完成了基于单二进制文件启动的web terminial应用程序,无任何第三方依赖,随用随启动。

本教程是前三篇基础教程的实战编程,结合了前端、后端、安全和网络开发等技术。

项目地址

Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
showme tty [flags] [command] [args]
eg: showme tty -w -r showme proxy http

Usage:
  showme tty [flags]

Flags:
  -a, --audit             is audit
  -d, --debug             debug log mode
  -h, --help              help for tty
  -m, --maxconnect int    max connect number
  -p, --password string   BasicAuth 密码
  -P, --port string       http port (default "8080")
  -r, --reconnect         is auto reconnect
  -u, --username string   BasicAuth 用户名
  -w, --write             is permit write

tty.png

重点技术列表

  • 静态文件打包到二进制文件中,比如html、js、css等
  • http转websocket后端技术启动
  • gin web框架的使用
  • cobra cli命令行工具使用
  • websocket协议的诸多问题分析和解决
  • xterm3.html单页面技术的开发和页面动态渲染技术
  • gin router路由策略与静态二进制文件路由策略的结合
  • web安全技术的引进,比如:Xsrf、BasicAuth、Audit、Base64
  • Golang 携程的实战应用
  • Makefile编译工具的深入使用

静态文件打包

将静态文件打包进golang的二进制执行文件利用的是bytes.Buffer缓存技术加上将文件硬编码到Code中去实现的,采用的开源工具包go-bindata和文件服务包go-bindata-assetfs

安装go-bindata makefile片段

1
2
3
4
bindata:
	@echo 安装预制环境
	go get -u github.com/jteeuwen/go-bindata/...
	go get -u github.com/elazarl/go-bindata-assetfs/...

生成硬编码Code

1
2
3
# 静态文件转go二进制文件
asset: bindata
	cd tty/static && go-bindata -o=../asset.go -pkg=tty ./

最后会在../目录生成asset.go文件,Packege名称为tty

http升级为websocket

多说不宜,直接上代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import "github.com/gorilla/websocket"

var upGrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

...

// 后端websocket服务
apiGroup.GET("/ws", func(c *gin.Context) {
    ...
    // 升级get请求为webSocket协议
    ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        return
    }
    defer ws.Close()

cobra cli命令行工具使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
	"github.com/lflxp/showme/tty"
	"github.com/spf13/cobra"
)

var (
	isAudit        bool
	isPermitWrite  bool
	MaxConnections int64
	isReconnect    bool
	isDebug        bool
	username       string
	password       string
	port           string
)

// ttyCmd represents the tty command
var ttyCmd = &cobra.Command{
	Use:   "tty",
	Short: "web terminial",
	Long: `showme tty [flags] [command] [args]
eg: showme tty -w -r showme proxy http`,
	Run: func(cmd *cobra.Command, args []string) {
		tty.ServeGin(port, username, password, args, isDebug, isReconnect, isPermitWrite, isAudit, MaxConnections)
	},
}

func init() {
	rootCmd.AddCommand(ttyCmd)
	ttyCmd.Flags().StringVarP(&username, "username", "u", "", "BasicAuth 用户名")
	ttyCmd.Flags().StringVarP(&password, "password", "p", "", "BasicAuth 密码")
	ttyCmd.Flags().StringVarP(&port, "port", "P", "8080", "http port")
	ttyCmd.Flags().BoolVarP(&isDebug, "debug", "d", false, "debug log mode")
	ttyCmd.Flags().BoolVarP(&isReconnect, "reconnect", "r", false, "is auto reconnect")
	ttyCmd.Flags().BoolVarP(&isPermitWrite, "write", "w", false, "is permit write")
	ttyCmd.Flags().BoolVarP(&isAudit, "audit", "a", false, "is audit")
	ttyCmd.Flags().Int64VarP(&MaxConnections, "maxconnect", "m", 0, "max connect number")
}

websocket协议的诸多问题分析和解决

  • 最头疼的【1002】invalid UTF-8 in Close frame

原因:因为websocket支持text和binary两种编码格式,对应129和130,一旦发现返回信息中有UTF8编码的往text协议类型发就会报错。(开源项目很多都是以text 129进行默认编码)

解决:

1
2
3
4
5
6
7
// 将所有返回结果包括UTF8编码的内容用base64进行编码,client解码再显示,避免了直接UTF8编码传输的报错
// Could not decode a text frame as UTF-8 的解决
safeMessage := base64.StdEncoding.EncodeToString([]byte(buf[:size]))
if err = this.write([]byte(safeMessage)); err != nil {
    log.Error(err.Error())
    return
}

html单页面动态渲染技术

主要用到了go template + 静态bytes.Buffer页面 + gin web渲染,具体方法在代码中体会。

前端xterm3.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
term.writeln('Welcome to \x1B[1;3;31mShowMe TTY\x1B[0m')
{{if .Reconnect}}
term.writeln('连接模式: \x1B[1;3;32mReconnect\x1B[0m')
{{- else}}
term.writeln('连接模式: \x1B[1;3;32mOnce\x1B[0m')
{{- end}}

{{if .Write}}
term.writeln('输入模式:\x1B[1;3;31m读/写\x1B[0m')
{{- else}}
term.writeln('输入模式:\x1B[1;3;31m读\x1B[0m')
{{- end}}

{{if .MaxC}}
term.writeln('连接状态:\x1B[1;3;34m{{.Conn}}/{{.MaxC}}\x1B[0m')
{{- else}}
term.writeln('连接总数:\x1B[1;3;34m{{.Conn}}\x1B[0m')
{{- end}}

term.writeln('启动参数: \x1B[1;3;30m{{ .Cmd }}\x1B[0m')
term.writeln('XsrfToken: {{ .Xsrf }}')

后端golang传值渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 主页
// 从内存取出然后渲染加载
indexhtml := multitemplate.New()
xterm3, err := Asset("xterm3.html")
if err != nil {
    log.Error(err.Error())
    return
}

t, err := template.New("index").Parse(string(xterm3))
if err != nil {
    log.Error(err.Error())
    return
}

indexhtml.Add("index", t)
router.HTMLRender = indexhtml
apiGroup.GET("/index", func(c *gin.Context) {
    newXsrf := utils.GetRandomSalt()
    xterm.XsrfToken.Store(newXsrf, time.Now().String())
    c.HTML(http.StatusOK, "index", gin.H{
        "host":      c.Request.RemoteAddr,
        "Reconnect": isReconnect,
        "Debug":     isdebug,
        "Write":     isPermitWrite,
        "MaxC":      MaxConnections,
        "Conn":      *xterm.Connections + 1,
        "Cmd":       strings.Join(cmds, " "),
        "Xsrf":      newXsrf,
    })
})

gin router路由策略与静态二进制文件路由策略的结合

关键代码

1
2
3
4
5
6
7
8
// 静态二进制文件
fs := assetfs.AssetFS{
    Asset:    Asset,
    AssetDir: AssetDir,
}
router.StaticFS("/static", &fs)
// 静态文件
// router.StaticFS("/static", http.Dir("./tty/static")

web安全技术的引进

  • XSRF

关键技术就是编码和流程解析,如下:

1. 前端请求页面
2. 后端生成XsrfToken并写入渲染页面
3. 前端按照: msgType + XsrfToken + Base64.encode(message)进行信息传递
4. 后端按照约定好的编码进行解析并验证XsrfToken
5. 成功则继续 失败退出
  • BasicAuth http常用用户密码认证
  • Audit 登录请求申请功能,用sqlite3进行数据存储和api查询

Golang 携程的实战应用

整个后端编程,Golang携程贯穿始终

  • http内部携程
  • websocket通信Send、Receive、HandlerClient均为携程运行
  • channel用于退出信号传递和连接携程管控

Makefile编译工具的深入使用

编译过程比较复杂,需要生成asset.go硬编码程序,依赖包等预制条件准备,代码编译和代码推送以及代码测试等工作,Makefile不二选择。实际文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
.PHONY: push pull install run clean asset tty build gopacket bindata

# 默认位置 以后都保持不变
push: asset pull
	git add .
	git commit -m "${m}"
	git push origin $(shell git branch|grep '*'|awk '{print $$2}')

pull:
	git pull origin $(shell git branch|grep '*'|awk '{print $$2}')

build: Makefile main.go asset
	go build
	chmod +x showme 
	./showme -h

install: Makefile main.go asset
	go install
	showme -h

gopacket: Makefile main.go asset
	go build -tags=gopacket
	chmod +x showme 
	./showme -h

# 静态文件转go二进制文件
asset: bindata
	cd tty/static && go-bindata -o=../asset.go -pkg=tty ./

run: main.go
	go run main.go static

# tty功能测试
tty: asset
	go run main.go tty -w -m 1 -d -a -u admin -p admin bash 
	# go run main.go tty -w -m 10 -r -d showme proxy http

bindata:
	@echo 安装预制环境
	go get -u github.com/jteeuwen/go-bindata/...
	go get -u github.com/elazarl/go-bindata-assetfs/...

clean:
	rm -f 123.mp4
	rm -f 1.db
	rm -f tty/asset.go
	rm -f showme

线上部署

最近又加了几个新功能:

  • https 的支持
  • aduit 增加了访问名单记录和查询结果优化
  • systemctl 部署showme tty

注意

用systemctl进行部署的时候会报TERM environment variable not set,这个需要在service文件里面指定环境变量TERM=xterm-256color

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@8.8.8.8:/etc/systemd/system# cat showme.service 
[Unit]
Description=showme
After=syslog.target
After=network.target

[Service]
# Modify these two values and uncomment them if you have
# repos with lots of files and get an HTTP error 500 because
# of that
###
#LimitMEMLOCK=infinity
#LimitNOFILE=65535
Type=simple
User=root
Group=root
WorkingDirectory=/tls
ExecStart=/usr/bin/showme tty -P 1234 -w -a -t -f -m 10 -u $user -p $pwd -c /tls/server.crt -k /tls/server.key
# ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
Environment=USER=root HOME=/opt TERM=xterm-256color

[Install]
WantedBy=multi-user.target

参考